How C++ Resolves A Function Call
How C++ Resolves A Function Call
preshing.com/20210315/how-cpp-resolves-a-function-call/
C is a simple language. You’re only allowed to have one function with each
name. C++, on the other hand, gives you much more flexibility:
You can have multiple functions with the same name (overloading).
You can overload built-in operators like + and == .
You can write function templates.
Namespaces help you avoid naming conflicts.
I like these C++ features. With these features, you can make str1 +
str2 return the concatenation of two strings. You can have a pair of 2D
points, and another pair of 3D points, and overload dot(a, b) to work
with either type. You can have a bunch of array-like classes and write a
single sort function template that works with all of them.
But when you take advantage of these features, it’s easy to push things too
far. At some point, the compiler might unexpectedly reject your code with
errors like:
error C2666: 'String::operator ==': 2 overloads have similar
conversions
note: could be 'bool String::operator ==(const String &) const'
note: or 'built-in C++ operator==(const char *, const char *)'
note: while trying to match the argument list '(const String, const
char *)'
Like many C++ programmers, I’ve struggled with such errors throughout
my career. Each time it happened, I would usually scratch my head, search
online for a better understanding, then change the code until it compiled.
But more recently, while developing a new runtime library for Plywood, I
was thwarted by such errors over and over again. It became clear that
despite all my previous experience with C++, something was missing from
my understanding and I didn’t know what it was.
1/11
hidden algorithm that runs for every function call at compile time.
This is how the compiler, given a function call expression, figures out
exactly which function to call:
These steps are enshrined in the C++ standard. Every C++ compiler must
follow them, and the whole thing happens at compile time for every
function call expression evaluated by the program. In hindsight, it’s
obvious there has to be an algorithm like this. It’s the only way C++ can
support all the above-mentioned features at the same time. This is what
you get when you combine those features together.
I imagine the overall intent of the algorithm is to “do what the programmer
expects”, and to some extent, it’s successful at that. You can get pretty far
ignoring the algorithm altogether. But when you start to use multiple C++
features, as you might when developing a library, it’s better to know the
rules.
So let’s walk through the algorithm from beginning to end. A lot of what
we’ll cover will be familiar to experienced C++ programmers. Nonetheless,
I think it can be quite eye-opening to see how all the steps fit together. (At
least it was for me.) We’ll touch on several advanced C++ subtopics along
the way, like argument-dependent lookup and SFINAE, but we won’t dive
too deeply into any particular subtopic. That way, even if you know
nothing else about a subtopic, you’ll at least know how it fits into C++’s
overall strategy for resolving function calls at compile time. I’d argue that’s
the most important thing.
Name Lookup
Our journey begins with a function call expression. Take, for example, the
expression blast(ast, 100) in the code listing below. This expression is
clearly meant to call a function named blast . But which one?
2/11
namespace galaxy {
struct Asteroid {
float radius = 12;
};
void blast(Asteroid* ast, float force);
}
struct Target {
galaxy::Asteroid* ast;
Target(galaxy::Asteroid* ast) : ast{ast} {}
operator galaxy::Asteroid*() const { return ast; }
};
The first step toward answering this question is name lookup. In this
step, the compiler looks at all functions and function templates that have
been declared up to this point and identifies the ones that could be referred
to by the given name.
3/11
above, the compiler finds three candidates:
The reason is because any time you use an unqualified name in a function
call – and the name doesn’t refer to a class member, among other things –
ADL kicks in, and name lookup becomes more greedy. Specifically, in
addition to the usual places, the compiler looks for candidate functions in
the namespaces of the argument types – hence the name “argument-
dependent lookup”.
The complete set of rules governing ADL is more nuanced than what I’ve
described here, but the key thing is that ADL only works with unqualified
names. For qualified names, which are looked up in a single scope, there’s
no point. ADL also works when overloading built-in operators like + and
== , which lets you take advantage of it when writing, say, a math library.
Interestingly, there are cases where member name lookup can find
candidates that unqualified name lookup can’t. See this post by Eli
Bendersky for details about that.
Some of the candidates found by name lookup are functions; others are
function templates. There’s just one problem with function templates: You
can’t call them. You can only call functions. Therefore, after name lookup,
the compiler goes through the list of candidates and tries to turn each
function template into a function.
4/11
In the example we’ve been following, one of the
candidates is indeed a function template:
5/11
template <typename T> void blast(T* obj, float force, typename
T::Units mass = 5000);
If this was the case, the compiler would try to replace the T in T::Units
with galaxy::Asteroid . The resulting type specifier,
galaxy::Asteroid::Units , would be ill-formed because the struct
galaxy::Asteroid doesn’t actually have a member named Units .
Therefore, template argument substitution would fail.
Overload Resolution
At this stage, all of the function templates found during name lookup are
gone, and we’re left with a nice, tidy set of candidate functions. This is
also referred to as the overload set. Here’s the updated list of candidate
functions for our example:
The next two steps narrow down this list even further
by determining which of the candidate functions are
viable – in other words, which ones could handle the function call.
6/11
Candidate 1
The caller’s first argument type galaxy::Asteroid* is an exact match.
The caller’s second argument type int is implicitly convertible to the
second function parameter type float , since int to float is a
standard conversion. Therefore, candidate 1’s parameters are compatible.
Candidate 2
The caller’s first argument type galaxy::Asteroid* is implicitly
convertible to the first function parameter type Target because Target
has a converting constructor that accepts arguments of type
galaxy::Asteroid* . (Incidentally, these types are also convertible in the
other direction, since Target has a user-defined conversion function
back to galaxy::Asteroid* .) However, the caller passed two arguments,
and candidate 2 only accepts one. Therefore, candidate 2 is not viable.
Candidate 3
Candidate 3’s parameter types are identical to
candidate 1’s, so it’s compatible too.
Like everything else in this process, the rules that control implicit
conversion are an entire subject on their own. The most noteworthy rule is
that you can avoid letting constructors and conversion operators
participate in implicit conversion by marking them explicit.
After using the caller’s arguments to filter out incompatible candidates, the
compiler proceeds to check whether each function’s constraints are
satisfied, if there are any. Constraints are a new feature in C++20. They let
you use custom logic to eliminate candidate functions (coming from a class
template or function template) without having to resort to SFINAE.
They’re also supposed to give you better error messages. Our example
doesn’t use constraints, so we can skip this step. (Technically, the standard
says that constraints are also checked earlier, during template argument
deduction, but I skipped over that detail. Checking in both places helps
ensure the best possible error message is shown.)
Tiebreakers
At this point in our example, we’re down to two viable functions. Either of
them could handle the original function call just fine:
7/11
Indeed, if either of the above functions was the only
viable one, it would be the one that handles the
function call. But because there are two, the compiler must now do what it
always does when there are multiple viable functions: It must determine
which one is the best viable function. To be the best viable function, one
of them must “win” against every other viable function as decided by a
sequence of tiebreaker rules.
In the example we’ve been following, the two viable functions have
identical parameter types, so neither is better than the other. It’s a tie. As
such, we move on to the second tiebreaker.
8/11
For example, consider the following two function templates:
template <typename T> void blast(T obj, float force);
template <typename T> void blast(T* obj, float force);
There are several more tiebreakers in addition to the ones listed here. For
example, if both the spaceship <=> operator and an overloaded comparison
operator such as > are viable, C++ prefers the comparison operator. And
if the candidates are user-defined conversion functions, there are other
rules that take higher priority than the ones I’ve shown. Nonetheless, I
believe the three tiebreakers I’ve shown are the most important to
remember.
Needless to say, if the compiler checks every tiebreaker and doesn’t find a
single, unambiguous winner, compilation fails with an error message
similar to the one shown near the beginning of this post.
We’ve reached the end of our journey. The compiler now knows exactly
which function should be called by the expression blast(ast, 100) . In
many cases, though, the compiler has more work to do after resolving a
function call:
9/11
If the function being called is a class member, the compiler must
check that member’s access specifiers to see if it’s accessible to the
caller.
If the function being called is a template function, the compiler
attempts to instantiate that template function, provided its definition
is visible.
If the function being called is a virtual function, the compiler
generates special machine instructions so that the correct override
will be called at runtime.
None of those things apply to our example. Besides, they’re outside the
scope of this post.
This post didn’t contain any new information. It was basically a condensed
explanation of an algorithm already described by cppreference.com, which,
in turn, is a condensed version of the C++ standard. However, the goal of
this post was to convey the main steps without getting dragged down into
details. Let’s take a look back to see just how much detail was skipped. It’s
actually kind of remarkable:
Yeah, C++ is complicated. If you’d like to spend more time exploring these
details, Stephan T. Lavavej produced a very watchable series of videos on
Channel 9 back in 2012. Check out the first three in particular. (Thanks to
Stephan for reviewing an early draft of this post.)
10/11
Now that I’ve learned exactly how C++ resolves a function call, I feel more
competent as a library developer. Compilation errors are more obvious. I
can better justify API design decisions. I even managed to distill a small set
of tips and tricks out of the rules. But that’s a subject for another post.
11/11