Hello! Welcome to your fourth lesson on C++ internals.
In our previous lessons, we laid some crucial groundwork. We saw how the compiler uses name mangling to create unique symbols for the linker and how references act as a safer, high-level abstraction over the pointers you know from C, while typically compiling down to the same machine code.
Today, we'll see how these concepts converge to enable a powerful C++ feature: function overloading. This lesson will not only teach you how to write overloaded functions but, more importantly, will demystify the complex algorithm the compiler uses to decide exactly which function to call. This process, known as overload resolution, is a cornerstone of C++ and understanding its rules is key to writing correct, unambiguous code and deciphering compiler errors.
Why Overloading? A Tale of Two Compilers
In C, every function must have a unique name. If you wanted to print an integer and a double, you'd need two differently named functions:
void print_int(int value);
void print_double(double value);
This is because the C compiler generates a simple symbol, like _print_int, for the linker. Having two functions with the same name would create a "duplicate symbol" error during linking.
C++, however, allows you to give both functions the same name, as long as their parameter lists differ. This is called function overloading.
void print(int value);
void print(double value);
print(42); // Calls the int version
print(3.14); // Calls the double version
How is this possible? The answer lies in name mangling, which we've touched on before. The C++ compiler encodes the function's parameter types directly into the symbol name. Let's see a practical demonstration of this.
The video 'Name Mangling In C++' from the CppNuts channel provides a concise demonstration using the nm utility, which lists symbols from object files.
Watch from the beginning to 01:31. The key takeaway is seeing how the two print functions with different parameters get compiled into two distinct symbols in the object file (_Z5printi and _Z5printd).
The video shows that the compiler transforms the C++ source code into symbols the linker can differentiate. For example, following the Itanium C++ ABI (used by GCC and Clang):

This mangling is the enabling mechanism for overloading. It ensures each overload has a unique identity. But it doesn't explain how the compiler chooses which one to call. That's the job of overload resolution.
The Algorithm: How the Compiler Chooses an Overload
When you write print(42);, the compiler follows a strict, multi-step algorithm at compile-time to find the best possible match among all the print functions it knows about.
Let's walk through this process. A fantastic article from Preshing on Programming, "How C++ Resolves a Function Call," provides a clear roadmap which we will follow.
How C++ Resolves a Function Call
This article provides a superb high-level overview of the entire function call resolution process. We'll read the first part to understand how the compiler builds its initial list of potential functions.
Read the sections 'How C++ Resolves a Function Call' and 'Name Lookup'. Focus on understanding that the first step is finding every function with the given name that is visible from the call site. Pay special attention to the concept of Argument-Dependent Lookup (ADL), a key part of C++ that can pull in candidates from the namespaces of the arguments themselves.
As the article explains, the process begins with name lookup, which creates a set of candidate functions. This set includes all functions with the right name in the current scope, plus any found via ADL.
Once the candidate set is built (including any functions generated from templates), the real overload resolution begins. This is a three-stage filtering and ranking process:
-
Find Viable Functions: The compiler iterates through the candidate set and keeps only the functions that could be called. A function is "viable" if it has the right number of parameters (or default arguments) and if there's a way to convert each argument from the call to the type of the corresponding parameter.
-
Rank Viable Functions: The compiler then ranks the viable functions to find the "best" one. This ranking is based on the "cost" of the implicit conversions required for each argument. An exact match is best, followed by promotions, and finally other standard conversions.
-
Apply Tie-breakers: If, after ranking, two or more functions are considered equally "best", the compiler uses a set of tie-breaker rules to pick a winner. If no single winner can be found, the call is ambiguous, and you get a compile error.
The CppCon talk "Back To Basics: Overload Resolution" gives an excellent deep dive into these rules with some non-obvious examples that really test your understanding.
Back To Basics: Overload Resolution - CppCon 2021
This video is a comprehensive guide to the nitty-gritty details of overload resolution. We'll watch a few key sections that cover the core logic.
First, watch from 17:17 to 23:44. This segment explains the first step: building the candidate list and filtering it down to a set of 'viable' functions. Next, and most importantly, watch from 24:14 to 35:25. This section covers the crucial 'ranking' step. Pay close attention to the hierarchy of conversions and the surprising result of the quiz at 30:04. Understanding why int to long vs. int to char is ambiguous reveals the strict, non-intuitive logic the compiler follows.
The key takeaway from the video is the ranking of implicit conversions:
- Exact Match: No conversion needed. This is the best. (e.g.,
inttoint, or an lvalue to a referenceT&). - Promotion: Converting a "smaller" integer type to
int. (e.g.,shorttoint). These are considered very cheap. - Conversion: All other standard conversions (e.g.,
inttofloat,inttolong long, pointer tobool). - User-Defined Conversion: Conversions involving class types (e.g., from a
const char*to astd::stringvia a constructor). - Ellipsis Conversion: Matching the
...in a C-style variadic function. This is the worst match.
A function is considered a "better" match than another if it has at least one argument that is a better match, and no other arguments that are a worse match.
Test your understanding!
Given the following overloaded functions and a function call:
void process(long val); // Overload 1
void process(double val); // Overload 2
void execute() {
process('a'); // 'a' is a character literal of type char
}
Which overload will the compiler choose, and why? Think about the conversion ranking.
Show answer
The compiler will call process(long val).
Here's why:
- The argument
'a'is of typechar. - Both
chartolongandchartodoubleare possible implicit conversions. - According to the C++ standard, a
charis an integral type. The conversion fromchartolongis an integral promotion/conversion. The conversion fromchartodoubleis a floating-point conversion. - The standard ranks integral conversions as better than floating-point conversions.
- Therefore,
process(long)is the best viable function.
Breaking the Tie
What happens if two functions are equally good matches? For example, a non-template function and a template function that instantiates to the exact same signature.
How C++ Resolves a Function Call
Let's return to the Preshing article to see the tie-breaker rules.
Read the sub-section 'Tiebreakers'. Focus on the three main rules presented: (1) Better-matching parameters, (2) Non-template wins, and (3) More specialized template wins. This explains the final step in the resolution process.
The main tie-breaking rules are:
- If one function is a non-template and the other is a template specialization, the non-template function is preferred.
- If both are template specializations, the more specialized template is preferred. For example, a template taking
T*is more specialized than one takingT.
If, after all these rules, there is still no single best function, the compiler issues an "ambiguous call" error. When you see this error, it's your job to guide the compiler, perhaps by using an explicit static_cast to make your intention clear.
Conclusion
Today we connected the dots between name mangling and function overloading, and then took a deep dive into the compiler's decision-making process.
Key Takeaways:
- Function Overloading allows multiple functions to share the same name, provided their parameter lists are different in type or number.
- Name Mangling is the underlying mechanism that makes this possible, by creating a unique, decorated name for each overload for the linker to use.
- Overload Resolution is the compile-time algorithm used to select the best function for a given call. It follows a strict sequence:
- Name Lookup: Find all candidate functions.
- Viability Check: Filter for functions that could possibly be called.
- Ranking: Find the best match(es) based on the cost of implicit argument conversions (Exact > Promotion > Conversion).
- Tie-Breaking: If necessary, apply rules like preferring non-templates over templates.
- An ambiguous call error means the compiler found more than one "best" match and cannot decide between them.
In our next lesson, we will explore const correctness. You'll see how the const keyword not only helps you write safer code but also participates in overload resolution, allowing you to provide different behaviors for const and non-const objects. This will build directly on the ranking and viability rules we learned today.