Foundations · Module 2 · Lesson 8 of 10
Overloading and Default Arguments
Give several functions one name, watch the compiler rank the candidates and sometimes refuse to choose, see the mangled symbols the linker receives, and use default arguments that the compiler fills in at the call site.
By the end you can
- Predict which overload the compiler picks for a given call
- Explain why overloading costs nothing at run time and how the linker tells overloads apart
- Use default arguments correctly and explain when they are filled in
- Recognize an ambiguous call and name two ways to fix it
01 · A question first
The statement std::cout << 65 prints 65, while std::cout << 'A' prints the letter A, although both values are stored as the same number. How does one operator know which job to do, and how does the linker keep many functions with the same name apart?
Make a guess, then step through the trace below to check it.
02 · Watch it run
void show(int n);void show(double x);void show(bool b); show(7);show(2.5);show('A');show(7L);- candidate 0show(int)
- candidate 1show(double)
- candidate 2show(bool)
Three functions share the name show. At each call the compiler, not the running program, compares the argument's type with every candidate and picks the best match. The three cells are the candidates.
Machine work: compile time: collect every function named show
03 · The idea
Start with the problem
You want a function show that prints an int, a double or a bool. Languages without overloading force a name per type: showInt, showDouble, showBool. You also want functions with optional details, such as a greeting that is "Hello" unless the caller says otherwise.
C++ solves both. You have already used the first idea without noticing: std::cout << 65 prints 65, while std::cout << 'A' prints A. Both values are the same number, as lesson 4 explained, and the stream prints them differently because it has a separate insertion function for int and for char. The compiler picks the right one from the type of the argument. This lesson explains how that choice works.
How the idea works
Overloading. Several functions may share a name if their parameter lists differ in number or in type. Such functions are overloads. The return type is not part of the distinction: int f(int) and double f(int) cannot both exist, because a call would not say which one is meant.
Overload resolution. At each call the compiler gathers the functions with that name, keeps those that can accept the arguments, and picks the best match by rank:
- An exact match needs no conversion.
- A promotion is a safe widening, such as
charorshorttoint, orfloattodouble. - A conversion is any other implicit change, such as
inttodouble,longtoint, or anything tobool. - A user-defined conversion, such as turning a string literal into a
std::string, ranks last.
If one candidate beats the rest, it is chosen. If several tie, the call is ambiguous, which is a compile error. If none can accept the arguments, the error is no matching function for call to ..., followed by notes that list the candidates, as lesson 6 described. Steps 2 to 5 of the trace show an exact match, a promotion and an ambiguity.
The type decides, not the value. Resolution looks only at the declared types of the arguments. That is why show('A') calls the int version and passes 65: the letter was promoted. If you want the character, you must provide a show(char) overload.
Behind the code: name mangling. The linker works with symbol names, and two functions cannot share one. So the compiler builds the name from the function name and its parameter types. With GCC and Clang, show(int) becomes _Z4showi, show(double) becomes _Z4showd, and so on (step 6). MSVC uses a different scheme. You can see the symbols with nm hello.o from lesson 1, and nm -C translates them back. The declaration extern "C" turns mangling off, which is how C++ code links with C code, since C has no overloading.
Default arguments. A parameter may have a default value, used when the caller leaves it out:
void greet(const std::string& name, const std::string& greeting = "Hello");
greet("Ada"); // same as greet("Ada", "Hello")
greet("Ada", "Welcome");Defaults must be the rightmost parameters, and they are written once, in the declaration, not again in the definition.
Which to use. A default fits one operation with an optional detail. Overloads fit different types, or genuinely different behavior. Combining the two can be ambiguous: with f(int) and f(int, int = 0), the call f(1) matches both equally well.
Why it is correct
The ranking rules aim at the closest match winning and at surprising conversions losing. Because the ranking depends only on static types, each call is bound to exactly one function at compile time, which is called static dispatch. A later module adds virtual functions, which choose at run time. Declaring a tie an error, rather than guessing, keeps the language safe: you are never silently handed a function you did not intend.
Mangling gives each overload a unique name at the level of the linker, so the one definition rule from lesson 1 applies to each signature separately. Defaults are plain syntactic help: the compiler rewrites the call, so the function keeps one fixed signature and never needs to ask whether an argument was supplied.
What it costs
Run time: nothing. Resolution is finished before the program exists, and the call is a direct call to one symbol. Mangled names make symbols longer, which has a negligible effect on file size.
Compile time. More candidates mean more work for the compiler. Heavily overloaded names, such as the stream insertion operator, are a small part of why compiling <iostream> takes time.
Readability. A large overload set can be hard to predict, so keep sets small and consistent.
Defaults live in the callers. Because the compiler pastes the default into each call, changing it in a header does not affect code that was already compiled. Every caller must be recompiled to see the new value, which matters in large projects and in libraries shipped as binaries.
Pitfalls and variations
Ambiguity from literals. A long or an unsigned argument against overloads for int and double is a classic tie. Cast the argument, add the missing overload, or write show(static_cast<int>(x)).
The surprising promotion. A character passed to an int overload prints as a number. Add a char overload when the letter is what you mean.
Defaults repeated. Writing the default in both the declaration and the definition is an error. GCC reports default argument given for parameter 2 of ... and adds a note that points to the previous specification.
Top-level const does not count. The overloads f(int) and f(const int) are the same function, because a copy of the argument is made either way, so declaring both is an error.
Variations. A function you want to forbid can be marked = delete, as in void show(long) = delete;, which turns a silent promotion or an ambiguity into a clear error. Templates, which come at the advanced level, generate a new overload for every type that is used. Operators such as << and + can be overloaded for your own types, which a later module covers.
Where it is used
The standard library is full of overloads: std::abs for integers and for floating-point numbers, many versions of std::to_string, std::string::find, and the insertion operator for every built-in type. Constructors of classes, which module 4 introduces, are almost always overloaded, and APIs with optional parameters use defaults throughout.
The next lesson, "Recursion: A Function That Calls Itself", puts the call stack from lesson 6 to work: one function, many frames at once, and a base case that stops them.
04 · Prove it
Question 1 of 4 · answer from memory
Functions `f(int)` and `f(double)` exist, and the call is `f('A')` with a char argument. What happens?
05 · Practice
Asked in interviews
Something wrong with this lesson? Report a problem