Default arguments and overloading coexist in C++ because they solve different problems. A default argument lets a caller leave a trailing parameter out. Overloading lets several functions share a name as long as their parameter lists differ. Both reduce the number of names you have to remember, and both come with rules that are worth learning before they surprise you.
Default arguments fill in the trailing parameters
A default argument is a value used when the caller does not supply one. Defaults must be the trailing parameters of the list.
#include
double apply_discount(double total, double percent = 10.0) {
return total - total * percent / 100.0;
}
int main() {
std::cout << "default 10%: " << apply_discount(100.0) << '\n';
std::cout << "custom 25%: " << apply_discount(100.0, 25.0) << '\n';
return 0;
}The output shows the default being used and then overridden:
default 10%: 90
custom 25%: 75The rule about trailing parameters is not a style preference — it is a requirement of the language. If a default were in the middle, a caller could not skip it while supplying the ones after it.
Defaults belong in the declaration, not the definition
When a function is declared in a header and defined in a source file, the default value goes in the declaration. Repeating it in the definition is an error.
#include
double apply_discount(double total, double percent = 10.0); // default here
int main() {
std::cout << apply_discount(50.0) << '\n';
return 0;
}
double apply_discount(double total, double percent) {
return total - total * percent / 100.0;
}The reason is that a default argument is part of the function's *type* as seen by the caller. The caller only ever sees the declaration, so that is where the information must live.
Overloading: one name, several signatures
Overloading lets you define several functions with the same name provided their parameter lists differ in type or number.
#include
#include
int measure(int centimetres) {
return centimetres;
}
int measure(double metres) {
return static_cast(metres * 100);
}
int measure(const std::string& text) {
return static_cast(text.length());
}
int main() {
std::cout << "int: " << measure(42) << '\n';
std::cout << "double: " << measure(1.75) << '\n';
std::cout << "string: " << measure("bakery") << '\n';
return 0;
}The compiler picks the overload that matches the argument types exactly. The output confirms the right one was chosen each time:
int: 42
double: 175
string: 6Note that the return type plays no part in the decision. Two overloads that differ only in their return type are a compile error, because the compiler would have no way to choose between them.
How the compiler chooses
The rules are, in order: an exact match wins; otherwise a conversion that is safe and lossless is preferred; otherwise the compiler looks for a conversion that any other candidate would also need. If two candidates are equally good, the call is ambiguous and the compiler complains.
A common surprise is the literal zero. Passing 0 to an overloaded function can be ambiguous, because the literal could be an int or a pointer. Cast it to the type you mean.
#include
#include
void print_order(int id) {
std::cout << "order " << id << '\n';
}
void print_order(const std::string& reference) {
std::cout << "reference " << reference << '\n';
}
int main() {
print_order(4821);
print_order("AB-4821");
print_order(static_cast(0));
return 0;
}When overloading becomes a mistake
Overloading is a tool for genuinely different operations, not for optional behaviour. A function with six overloads that differ only by one defaulted parameter is usually a sign that the design wants a struct or a default argument instead.
Two rules keep overloading sane:
- Overloads should take the same kind of action. If one overload sorts and another prints, that is two functions wearing a trench coat.
- Do not overload on a pointer and a reference of the same type. That pair is ambiguous for many argument types and produces confusing error messages.
Overloading a function with different constness
Two overloads that differ only in const are a common and useful pattern. One takes a value it may modify, the other takes a value it must not.
#include
#include
void announce(std::string& order) {
order = "ORDER " + order;
}
void announce(const std::string& order) {
std::cout << "order (read only): " << order << '\n';
}
int main() {
std::string reference = "4821";
announce(reference);
std::cout << reference << '\n';
const std::string fixed = "AB-99";
announce(fixed);
return 0;
}The non-const version is chosen for a mutable string and the const version for a constant one. The call site reads the same either way, and the output shows both paths:
ORDER 4821
order (read only): AB-99A practical combination
Defaults and overloading work well together when the overloads differ in shape and the defaults cover the variations within one shape.
#include
std::string describe(int tables, bool outdoor = false) {
std::string result = std::to_string(tables) + " tables";
if (outdoor) {
result += " outdoors";
}
return result;
}
int main() {
std::cout << describe(4) << '\n';
std::cout << describe(4, true) << '\n';
return 0;
}The output shows the default in action and then the override:
4 tables
4 tables outdoorsQuick reference
- Default arguments must be the trailing parameters, and must appear in the declaration only.
- Overloads must differ in parameter types or count, not in return type alone.
- An exact match beats a conversion; two equally good candidates are ambiguous.
- Casting a literal such as
0can remove an ambiguity. - Do not overload purely to add optional behaviour — use a default argument instead.
- Overloads should perform the same kind of action on different types.
Try it yourself: write a function that formats an order total and a function that formats a delivery time, both called [[format]], so that one takes a [[double]] and the other a [[std::string]]. Then add a default argument to the total version for the currency symbol, and finally write a call that is deliberately ambiguous and see how the compiler describes the problem.