Overloads and default arguments
Functions and library types11 min readLesson 5 of 5
5.1 One name, several signatures
Function overloading lets several functions share a name when their parameter lists differ. The compiler chooses a version from the argument types at the call site. Overloads are resolved during compilation, so there is no runtime lookup.
Return type does not participate in overload resolution. int parse(std::string) and double parse(std::string) cannot coexist, because a call with one string argument does not tell the compiler which return type you want. Parameter count, parameter types, and the const qualifier on a member function are the parts that distinguish overloads.
#include <iostream>
#include <string>
void describe(int value) {
std::cout << "Integer: " << value << "\n";
}
void describe(double value) {
std::cout << "Decimal: " << value << "\n";
}
void describe(const std::string& value) {
std::cout << "Text: " << value << "\n";
}
int main() {
describe(7);
describe(2.5);
describe("notes");
return 0;
}Output
Integer: 7 Decimal: 2.5 Text: notes
5.1.1 How the compiler ranks a call
The compiler collects the candidate functions with the right name, then ranks the conversions needed for each argument. The order runs from an exact match, through promotions such as short to int, through standard conversions such as int to double, to user-defined conversions such as a constructor call.
A call is ambiguous when two candidates need equally good conversions. void show(long) and void show(double) both require a standard conversion from an int, so show(1) does not compile. Adding an int overload resolves that call, and it also makes the intent of the overload set clearer.
Keep overloads for operations with the same meaning. A set of print functions that all write a value is easy to use. Functions that happen to share a name but behave differently make the call site misleading.
#include <iostream>
void show(int value) {
std::cout << "int " << value << "\n";
}
void show(double value) {
std::cout << "double " << value << "\n";
}
int main() {
show(4);
show(4.0);
short small = 3;
show(small);
return 0;
}Output
int 4 double 4 int 3
5.1.2 Default arguments
A default argument gives a parameter a value that the caller may omit. The parameter still exists, and the function body can use it like any other parameter. Defaults must come after all parameters without defaults, because arguments are matched by position.
Write the default once. When a function is declared and then defined, put the default in the declaration that callers include, and leave it out of the definition. Repeating it is a compile error. A default is evaluated at each call, and names inside it are looked up where the default is declared.
#include <iostream>
#include <string>
void print_rule(const std::string& label, char mark = '-', int width = 12) {
std::cout << label << "\n";
for (int i = 0; i < width; ++i) {
std::cout << mark;
}
std::cout << "\n";
}
int main() {
print_rule("Defaults");
print_rule("Custom", '=');
print_rule("Wide", '*', 6);
return 0;
}Output
Defaults ------------ Custom ============ Wide ******
5.1.3 Defaults and overloads together
Defaults and overloads can create a set of calls that looks reasonable and is ambiguous. void draw(int size) and void draw(int size, int border = 0) both accept draw(10), so the compiler rejects the call. The same problem appears when a default lets an overload match a call that another overload already matches exactly.
Changing a default changes the behavior of every caller that omitted the argument, and those callers must be recompiled. A named constant or a small configuration struct can make an important default visible. Use a default when most calls want the same value, and require the argument when the choice matters.
#include <iostream>
#include <string>
std::string repeat(const std::string& text, int times = 1) {
std::string result;
for (int i = 0; i < times; ++i) {
result += text;
}
return result;
}
int main() {
std::cout << repeat("ha") << "\n";
std::cout << repeat("ha", 3) << "\n";
return 0;
}Output
ha hahaha
5.1.4 Where overloads become confusing
Two overloads that differ only in whether a reference parameter is const can behave differently for the same source expression. void tag(int&) and void tag(const int&) accept the same variable, and the non-const version wins for a modifiable lvalue. A literal or a const object selects the const version.
Overloads that differ by a single numeric type invite accidental conversions. A float argument can promote to double or convert to int, and the ranking may not match the reader's guess. If two numeric overloads are genuinely needed, test calls with the types your code actually uses. In many cases one function template would express the shared behavior with less repetition.
#include <iostream>
void label(int& value) {
std::cout << "writable " << value << "\n";
}
void label(const int& value) {
std::cout << "read only " << value << "\n";
}
int main() {
int count = 3;
const int limit = 4;
label(count);
label(limit);
label(9);
return 0;
}Output
writable 3 read only 4 read only 9
5.1.5 Rules worth remembering
- A default belongs in one declaration only.
- Parameters with defaults must be trailing.
- A default cannot refer to a local variable or to another parameter.
- Overloads plus defaults can make a call ambiguous.
- Defaults are not part of the function type, so two declarations that differ only in a default are the same function.
If a function needs several optional settings, a small options struct with member initializers often reads better than five defaulted parameters. The call site then names each setting it changes.
5.1.6 Practice
- Write three
print_valueoverloads forint,double, andconst std::string&. - Add an
intoverload that resolves a call which was ambiguous without it. - Write a function with two trailing defaults and call it with zero, one, and two explicit arguments.
- Move a default argument from a definition to a declaration and confirm that the program still builds.
- Try
f(3)withf(int)andf(int, int = 0)and read the ambiguity error.
