1. Introduction
When you start writing real code (not only solving one‑screen exercises), a practical problem quickly appears: functions either multiply like rabbits, or they become 8‑parameter "combines". The first is hard to read, the second is inconvenient to call. Overloading and default arguments are two tools that help make your function APIs more human-friendly: you call the same action with the same name, and the compiler picks the details for you.
Imagine you're building a console app called TaskPad — a mini task list. We already have printing utilities, menu, string helpers and std::vector<std::string>. And suddenly you want to print a "separator" made of '-' or '=', and sometimes of length 20, sometimes 40. You could write four functions like print_line_dash_20(), print_line_dash_40()… but we're not carving a medieval manuscript in stone.
Overloading: one meaning — several call forms
Overloading (overload) is a situation where several functions with the same name exist in one scope, but with different parameters. Two functions are considered different if they differ in parameter count, types, or order. The idea of overloading is to give one action one name and let the compiler handle the "call form" details.
Let's start with the most grounded example: printing a separator line.
#include <iostream>
void print_line(char ch, int count) {
for (int i = 0; i < count; ++i) std::cout << ch;
std::cout << '\n';
}
void print_line(int count) { // overload: different parameter list
print_line('-', count);
}
Now the code reads nicely:
print_line(20); // print the "normal" line made of '-'
print_line('=', 20); // print the "bold" line made of '='
Note an important detail: the second version print_line(int) doesn't duplicate the loop — it simply uses the first version as the "base". This reduces copy‑paste and the chance you'll accidentally forget the '\n' in one version (and later suffer because the console output got glued together).
How the compiler chooses an overload
In practice overloading scares beginners not because of syntax but because of the question: "how does the compiler know which function I meant?" Answer: the compiler builds a list of candidates by name and then selects the variant that best matches the argument types. If several are equally good — it's an ambiguity error.
It's useful to keep a very simple mental model of selection:
| Call situation | What usually happens |
|---|---|
| There is a version with an exact type match | It almost always wins |
| No exact match, but a type can be converted (for example, int → double) | The compiler may choose such an overload |
| Two overloads fit "about equally" | Error: the call is ambiguous |
| None fits | Error: no suitable function |
Let's look at an example:
#include <iostream>
void show(int x) {
std::cout << "int: " << x << '\n';
}
void show(double x) {
std::cout << "double: " << x << '\n';
}
int main() {
show(10); // int: 10
show(3.14); // double: 3.14
}
Here it's transparent: 10 is an int, 3.14 is a double.
Now an example where something unexpected can happen. If you make overloads too similar, the compiler will sometimes honestly say: "I'm not a mind reader."
To avoid turning your code into a detective story, a practical rule is: overloads should differ enough that the choice is obvious to both the reader and the compiler.
Restriction: you cannot overload only by return type
At this point someone often gets the idea: "Can I make int parse(...) and bool parse(...), and the compiler will pick based on where I assign it?" Unfortunately, no. In C++ an overload cannot differ only by return type — it must differ in parameters. That makes sense: when you write parse("123"), the compiler has to pick the function right there, not guess what you'll do with the result later.
Here is an example that doesn't compile:
#include <string>
int parse(std::string s) {
return 0;
}
bool parse(std::string s) { // error: differs only by return type
return true;
}
So if you need two different results — either change the name (parse_int, parse_bool), or change parameters (for example, add a parsing mode), or design the return value differently (but that's a more advanced topic).
2. Default arguments: make the "typical case" short
Default arguments solve a different problem: not "two functions" but "one function that's usually called the same way". You can specify default values for some parameters so callers can omit them.
Back to the separator line. Instead of overloads we can write one function:
#include <iostream>
void print_line(char ch = '-', int count = 20) {
for (int i = 0; i < count; ++i) std::cout << ch;
std::cout << '\n';
}
int main() {
print_line(); // prints 20 characters '-'
print_line('='); // prints 20 characters '='
print_line('=', 8); // prints 8 characters '='
}
Here there is a feeling of a "convenience button": the common case ('-' and 20) requires no parameters, and unusual variants are specified explicitly.
Important rule: default values can be provided only for trailing parameters (i.e., on the right). From a common-sense perspective this also makes sense: if you skip a middle argument, how would the compiler know which one you meant to omit?
Here's an example of what you cannot do:
// not allowed: a parameter without default follows a parameter with default
void f(int a = 10, int b) { }
Where to put default values
When you use prototypes (function declarations before main), a practical question arises: where to put the = ...? A good habit is to keep the function "contract" in one place. So default values are usually written in the declaration (prototype), while the definition keeps a "clean" signature without repetition.
An example of "correct and tidy" style:
#include <iostream>
#include <string>
void greet(std::string name = "World"); // default in the declaration
int main() {
greet(); // Hello, World!
greet("Alice"); // Hello, Alice!
}
void greet(std::string name) { // without "= ..."
std::cout << "Hello, " << name << "!\n";
}
Why is that more convenient? Because if you accidentally give different default values in different places, you'll get either a compile error or (in more complex cases) very strange behavior and lots of pain. Keeping defaults in one place is like keeping your house keys in one pocket: you could do otherwise, but why?
3. What to choose and where the trap is
These two tools often solve similar problems but feel different. Overloading is usually better when you have truly different scenarios and want signatures to look and read differently. Default arguments are better when there is one scenario but "settings" are the same 80% of the time.
Compare on the example of printing a menu title in TaskPad. We want to print a frame and text. Sometimes the default width is 30, sometimes 50.
Variant with default:
#include <iostream>
#include <string>
void print_title(std::string title, int width = 30) {
std::cout << '[' << title << "] width=" << width << '\n';
}
Variant with overloading:
#include <iostream>
#include <string>
void print_title(std::string title, int width) {
std::cout << '[' << title << "] width=" << width << '\n';
}
void print_title(std::string title) {
print_title(title, 30);
}
Both variants are fine. But in terms of feel:
Overloading makes the "simple call" a separate function, which can be easier to explain to beginners: one function is "simple", the other is "extended". A default argument makes one function more flexible but requires remembering the rule about trailing parameters.
Pitfall: overloads + defaults and ambiguity
The most common reason for "I don't understand, it worked yesterday" is that you combined overloads and default arguments so that the same call matches two variants at once. The compiler won't pick "what you meant" — it will emit an error (and be right).
Classic example:
#include <iostream>
int area(int side) {
return side * side;
}
int area(int w, int h = 1) {
return w * h;
}
int main() {
// std::cout << area(5) << '\n'; // ambiguous: both functions match
std::cout << area(5, 2) << '\n'; // ok
}
The call area(5) matches both area(int) directly and area(int, int) because the second parameter can be defaulted to 1. The compiler is not obliged to guess what you meant.
Practical rule: if you use default arguments, try not to make an overload that covers the same call forms. Conversely: if you make overloads, don't add defaults that create identical call forms.
4. TaskPad example: making printing and input more convenient
Now we'll assemble a few small functions you can use in TaskPad. We won't complicate the architecture or dive into reference passing — for now everything is straightforward: small functions, clear parameters, predictable calls.
To start — printing a header and a line.
#include <iostream>
#include <string>
void print_line(char ch = '-', int count = 30) {
for (int i = 0; i < count; ++i) std::cout << ch;
std::cout << '\n';
}
void print_header(std::string title) {
print_line('=');
std::cout << title << '\n';
print_line('=');
}
Now main can look like a scenario instead of a "wall of output":
print_header("TaskPad");
print_line(); // the usual line
print_line('.', 10); // a shorter line with a different character
The next everyday piece: printing a menu. This is a good example of default arguments helping to avoid "parameter spam". We want to print a numbered menu and have a default width of 30.
#include <iostream>
void print_menu(int width = 30) {
print_line('-', width);
std::cout << "1) Add task\n";
std::cout << "2) List tasks\n";
std::cout << "0) Exit\n";
print_line('-', width);
}
Note: this is a good example of a default argument because "menu width" is a setting that's almost always the same but sometimes you want it wider.
Now add something you constantly meet in console apps: "read a command". We won't go into "robust input with error handling", but we can make a friendly interface: by default we show the prompt "> ".
#include <iostream>
#include <string>
std::string read_line(std::string prompt = "> ") {
std::cout << prompt;
std::string s;
std::getline(std::cin, s);
return s;
}
Usage:
std::string cmd = read_line(); // prints "> "
std::string name = read_line("Task name: "); // prints "Task name: "
Now a small point about overloading: sometimes you want read_line() to return an empty string and that's fine, but sometimes you want "non-empty input". We can provide an overload that accepts a flag requiring non-empty input, and in the bad case repeats the prompt. We won't implement full validation — just show the idea.
#include <iostream>
#include <string>
std::string read_line(std::string prompt) {
std::cout << prompt;
std::string s;
std::getline(std::cin, s);
return s;
}
std::string read_line(std::string prompt, bool non_empty) {
while (true) {
std::string s = read_line(prompt);
if (!non_empty || !s.empty()) return s;
std::cout << "Please enter non-empty text.\n";
}
}
And calls look meaningful:
std::string a = read_line("> "); // simple version
std::string b = read_line("Task name: ", true); // require non-empty
Here overloading is appropriate because the scenarios are different: "just read" and "read but non-empty". You could use a default argument bool non_empty = false, but beginners often have cognitive pain: "what does true as the second parameter mean?" Overloading can be more self-documenting when different call forms represent different behaviors.
Finally, an example of how you can badly combine overloading and defaults, and why we avoid that in TaskPad. If you write:
std::string read_line(std::string prompt = "> ");
std::string read_line();
you can very easily reach situations where read_line() can be called in different ways, and then errors and surprises begin. So we pick one style for a family of functions.
5. Mini decision chart
When in doubt, it's useful to have a short "mental flowchart". Not because you must draw diagrams every time (although sometimes it's nice), but because it disciplines the design.
flowchart TD
A["I want to make a function call more convenient"] --> B{"Is this one scenario with settings?"}
B -->|Yes| C["Default arguments"]
B -->|No| D{"Are these different scenarios with different call forms?"}
D -->|Yes| E["Overload"]
D -->|No| F["Better to rename the function or simplify the interface"]
And an important note (mentally): "Don't mix overloads and default arguments so that the same call matches two variants."
6. Common mistakes
Mistake #1: trying to overload only by return type.
It looks tempting because "the result is different", but the compiler chooses a function by name and parameters. If the parameters are the same, two functions conflict and the code won't compile. In such cases it's better to change the name or change the parameter list so the choice is unambiguous.
Mistake #2: default arguments are not placed on the right.
Beginners often want to "make the middle optional", but in C++ defaults are allowed only for trailing parameters. If a parameter without a default follows one with a default, the signature breaks and leads to a compile error. Fix by reordering parameters or redesigning the interface.
Mistake #3: combining overloads and a default argument that creates an ambiguous call.
The most unpleasant mistake, because it looks logical until you see the compiler message. Typical scenario: you have f(int) and f(int, int = 0), and suddenly f(5) becomes ambiguous. That's not a compiler bug — it's your interface giving two different meanings for the same form. In such places choose either an overload without defaults or a single function with defaults, but not both.
Mistake #4: default values scattered across the code.
When some defaults are specified in one declaration, others in another, and the definition "also knows something", you get a situation where the function contract isn't readable in one place. For maintainability this is almost guaranteed trouble. It's much safer to keep defaults in one declaration (usually the prototype) and keep the definition without repeats.
Mistake #5: overloads are too similar and the reader can't see the difference.
Even if the compiler can choose the right overload, a human later will read the code and try to understand intent. If the difference between overloads doesn't reflect different scenarios, you'll end up with an API that's a riddle rather than convenient. In that case it's better to rename functions or make a single function with parameters that explicitly express the setting.
GO TO FULL VERSION