Ready to practice?
Sign up to access interactive coding exercises and track your progress.
Overload Resolution
Predict which overload the compiler selects and resolve ambiguity errors.
Understanding Overload Resolution
In the previous lesson, we learned how to differentiate overloaded functions. But differentiation is only part of the story. When you call an overloaded function, the compiler must determine which version to use. This process is called overload resolution.
For functions with unique names, the compiler either finds a match or doesn't. With overloaded functions, multiple candidates may exist, and the compiler must choose the best one.
Simple Cases: Exact Type Matches
When argument types match parameter types exactly, resolution is straightforward:
#include <iostream>
void display(int value)
{
std::cout << "Integer: " << value << '\n';
}
void display(double value)
{
std::cout << "Double: " << value << '\n';
}
int main()
{
display(42); // calls display(int)
display(3.14); // calls display(double)
return 0;
}
But what happens when there's no exact match?
#include <iostream>
void display(int value)
{
std::cout << "Integer: " << value << '\n';
}
void display(double value)
{
std::cout << "Double: " << value << '\n';
}
int main()
{
display('z'); // char doesn't exactly match - which function?
display(100L); // long doesn't exactly match - which function?
return 0;
}
A char or long can be converted to int or double, but which conversion should the compiler choose?
The Resolution Process
The compiler follows a specific sequence to find the best match. At each step, it applies certain conversions to the arguments and checks for matches. Three outcomes are possible:
- No match found: Move to the next step
- Exactly one match found: Resolution complete
- Multiple matches found: Compilation error (ambiguous call)
If all steps are exhausted without finding a match, the compiler issues an error.
Step-by-Step Matching Sequence
Step 1: Exact match
The compiler first looks for an exact match or a match through trivial conversions (like adding const or converting to a reference):
void process(int value)
{
}
void process(double value)
{
}
int main()
{
process(10); // exact match with process(int)
process(2.7); // exact match with process(double)
return 0;
}
Trivial conversions include:
- Adding
constqualification - Converting non-reference to reference
- Converting lvalue to rvalue
Example:
void handle(const int value)
{
}
void handle(const double& value)
{
}
int main()
{
int num{ 5 };
handle(num); // num converted to const int (trivial)
double price{ 9.99 };
handle(price); // price converted to const double& (trivial)
return 0;
}
Step 2: Numeric promotion
If no exact match exists, the compiler tries promoting smaller types to standard types (char to int, float to double, etc.):
void calculate(int value)
{
}
void calculate(double value)
{
}
int main()
{
calculate('m'); // char promoted to int
calculate(true); // bool promoted to int
calculate(3.5f); // float promoted to double
return 0;
}
Step 3: Numeric conversion
Without a promotion match, the compiler applies numeric conversions (like int to double or char to double):
#include <string>
void transform(double value)
{
}
void transform(std::string value)
{
}
int main()
{
transform('x'); // 'x' converted to double (no int or string version exists)
return 0;
}
Numeric promotions always take priority over numeric conversions. The compiler prefers promotions because they're safer and more predictable.
Step 4: User-defined conversions
Classes can define custom conversions. The compiler uses these if no built-in conversion succeeds:
class Temperature
{
public:
operator int() { return 72; } // conversion to int
};
void report(int degrees)
{
}
void report(double degrees)
{
}
int main()
{
Temperature temp{};
report(temp); // Temperature converted to int
return 0;
}
Step 5: Ellipsis matching
Functions with ellipsis parameters (...) match as a last resort.
Step 6: No match
If all steps fail, the compiler generates an error.
Ambiguous Matches
An ambiguous match occurs when multiple functions match equally well at the same step. The compiler cannot choose between them:
void calculate(int value)
{
}
void calculate(double value)
{
}
int main()
{
calculate(50L); // Error: ambiguous!
return 0;
}
The long value 50L can be converted to either int or double. Both conversions are numeric conversions (step 3), so neither is better. The compiler reports an ambiguous call.
Another example:
void format(unsigned int value)
{
}
void format(float value)
{
}
int main()
{
format(10); // Error: int converts to unsigned int OR float
format(6.28); // Error: double converts to unsigned int OR float
return 0;
}
The integer 10 could become unsigned int or float, and the double 6.28 could become unsigned int or float. All are numeric conversions, creating ambiguity.
Resolving Ambiguity
Option 1: Add a matching overload
Define a version that exactly matches your argument types:
void format(unsigned int value)
{
}
void format(float value)
{
}
void format(int value) // Add this
{
}
int main()
{
format(10); // Now matches format(int) exactly
}
Option 2: Cast the argument
Explicitly cast to the desired type:
int quantity{ 10 };
format(static_cast<unsigned int>(quantity)); // calls format(unsigned int)
Option 3: Use literal suffixes
For literals, use suffixes to specify the exact type:
format(10u); // 'u' makes it unsigned int - exact match
format(6.28f); // 'f' makes it float - exact match
Multiple Parameter Matching
With multiple parameters, the compiler evaluates each argument independently. The chosen function must match at least as well as all others for every parameter, and better than all others for at least one parameter:
#include <iostream>
void output(char letter, int number)
{
std::cout << "Version A\n";
}
void output(char letter, double number)
{
std::cout << "Version B\n";
}
void output(char letter, float number)
{
std::cout << "Version C\n";
}
int main()
{
output('k', 'p'); // calls output(char, int)
return 0;
}
All three functions match the first parameter (char) exactly. For the second parameter ('p', which is type char):
output(char, int): Matches via promotion (char to int)output(char, double): Matches via conversion (char to double)output(char, float): Matches via conversion (char to float)
Promotion beats conversion, so output(char, int) is the clear winner.
Overload resolution happens independently for each parameter. The winning function must be at least as good for all parameters and strictly better for at least one.
Summary
Overload resolution determines which function gets called when multiple overloads exist:
Resolution Steps (in order):
- Exact match - Direct type match or trivial conversions
- Numeric promotion - Small types promoted to standard types (
chartoint,floattodouble) - Numeric conversion - Any numeric type conversion
- User-defined conversion - Custom class conversions
- Ellipsis match - Variadic functions (last resort)
Ambiguous Matches:
- Occur when multiple functions match equally well at the same step
- Result in a compilation error
- Can be resolved by: adding exact-match overloads, explicit casts, or literal suffixes
Multiple Parameters:
- Each parameter is evaluated independently
- The winning function must be at least as good for all parameters
- Must be strictly better for at least one parameter
Overload Resolution - Quiz
Test your understanding of the lesson.
Practice Exercises
Function Overload Resolution
Understand overload resolution by creating functions demonstrating exact matches, promotions, and conversions.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!