In the previous lesson ( 8.11 -- Function overload differentiation ), we discussed which attributes of a function are used to differentiate overloaded functions from each other. If an overloaded function is not properly differentiated from the other overloads of the same name, then the compiler will issue a compile error.
However, having a set of differentiated overloaded functions is only half of the picture. When any function call is made, the compiler must also ensure that a matching function declaration can be found.
With non-overloaded functions (functions with unique names), there is only one function that can potentially match a function call. That function either matches (or can be made to match after type conversions are applied), or it doesn’t (and a compile error results). With overloaded functions, there can be many functions that can potentially match a function call. Since a function call can only resolve to one of them, the compiler has to determine which overloaded function is the best match. The process of matching function calls to a specific overloaded function is called overload resolution .
In simple cases where the type of the function arguments and type of the function parameters match exactly, this is (usually) straightforward:
#include <iostream>
void print(int x)
std::cout << x << '\n';
void print(double d)
std::cout << d << '\n';
int main()
print(5); // 5 is an int, so this matches print(int)
print(6.7); // 6.7 is a double, so this matches print(double)
return 0;
}
But what happens in cases where the argument types in the function call don’t exactly match the parameter types in any of the overloaded functions? For example:
#include <iostream>
void print(int x)
std::cout << x << '\n';
void print(double d)
std::cout << d << '\n';
int main()
print('a'); // char does not match int or double
print(5L); // long does not match int or double
return 0;
}
Just because there is no exact match here doesn’t mean a match can’t be found -- after all, a
char
or
long
can be implicitly type converted to an
int
or a
double
. But which is the best conversion to make in each case?
In this lesson, we’ll explore how the compiler matches a given function call to a specific overloaded function.
Resolving overloaded function calls
When a function call is made to an overloaded function, the compiler steps through a sequence of rules to determine which (if any) of the overloaded functions is the best match.
At each step, the compiler applies a bunch of different type conversions to the argument(s) in the function call. For each conversion applied, the compiler checks if any of the overloaded functions are now a match. After all the different type conversions have been applied and checked for matches, the step is done. The result will be one of three possible outcomes:
- No matching functions were found. The compiler moves to the next step in the sequence.
- A single matching function was found. This function is considered to be the best match. The matching process is now complete, and subsequent steps are not executed.
- More than one matching function was found. The compiler will issue an ambiguous match compile error. We’ll discuss this case further in a bit.
If the compiler reaches the end of the entire sequence without finding a match, it will generate a compile error that no matching overloaded function could be found for the function call.
The argument matching sequence
Step 1) The compiler tries to find an exact match. This happens in two phases. First, the compiler will see if there is an overloaded function where the type of the arguments in the function call exactly matches the type of the parameters in the overloaded functions. For example:
void print(int)
void print(double)
int main()
print(0); // exact match with print(int)
print(3.4); // exact match with print(double)
return 0;
}
Because the
0
in the function call
print(0)
is an int, the compiler will look to see if a
print(int)
overload has been declared. Since it has, the compiler determines that
print(int)
is an exact match.
Second, the compiler will apply a number of trivial conversions to the arguments in the function call. The trivial conversions are a set of specific conversion rules that will modify types (without modifying the value) for purposes of finding a match. For example, a non-const type can be trivially converted to a const type:
void print(const int)
void print(double)
int main()
int x { 0 };
print(x); // x trivially converted to const int
return 0;
}
In the above example, we’ve called
print(x)
, where
x
is an
int
. The compiler will trivially convert
x
from an
int
into a
const int
, which then matches
print(const int)
.
For advanced readers
Converting a non-reference type to a reference type (or vice-versa) is also a trivial conversion.
Matches made via the trivial conversions are considered exact matches.
Step 2) If no exact match is found, the compiler tries to find a match by applying numeric promotion to the argument(s). In lesson (
8.1 -- Implicit type conversion (coercion)
), we covered how certain narrow integral and floating point types can be automatically promoted to wider types, such as
int
or
double
. If, after numeric promotion, a match is found, the function call is resolved.
For example: