AI InterviewSearch…Search problems, topics, or pages…Ctrl KGet started
Skip to content

04 Functions / Lesson 3 of 6

Overloading and default arguments

13 min · +10 XP · 2 exercises · 7 questions

Read the text13 min
Exercises0/2
Quiz7 q

Quick answer

What is function overloading in C++?

Function overloading lets several C++ functions share one name as long as their parameter lists differ in number or types, such as area(long long) and area(double). At each call the compiler ranks the viable overloads by conversion — exact match, promotion, standard conversion, then user-defined conversion — and picks the best. The return type never distinguishes overloads, and a tie is a compile error.

Two functions may share a name if their parameter lists differ; a parameter may have a value the caller can leave out. Both features exist to make call sites read naturally — area(4), area(3, 5), frame("hi") — and both have rules that surprise people who have only seen them work. This lesson settles how the compiler picks an overload (and when it refuses to), the string and pointer cases that pick the wrong one, where a default argument may be written, and when each feature is the clearer choice.

What overloading is

long long area(long long side);                     // a square
long long area(long long width, long long height);  // a rectangle
double area(double radius);                         // a circle

Three functions, one name, told apart by their signatures — the parameter count and types. The return type plays no part: int f() and double f() in the same scope is an error, "ambiguating new declaration", because a call f(); could not choose between them.

Overloading exists so that one idea gets one name. std::to_string has nine overloads, std::abs has one per numeric type, std::sort has two — with and without a comparator. A good overload set does the same thing for every parameter list; a set whose members do different things is a naming problem dressed as a language feature.

How the compiler chooses

At a call, the compiler collects every function with that name that is visible (the candidates), discards the ones the arguments cannot be converted to (leaving the viable ones), and ranks the survivors by how good the conversion for each argument is:

  1. Exact match — the argument's type, or a trivial adjustment (adding const to what a reference names, array to pointer).
  2. Promotion — char, short, bool and small enums to int; float to double.
  3. Standard conversion — any other built-in conversion: int to double, double to int, long to int, integer to bool, a pointer to bool, nullptr to a pointer.
  4. User-defined conversion — a constructor or conversion operator, such as const char* to std::string.

The best viable function must be strictly better on at least one argument and no worse on the rest. If two tie, the call is ambiguous and does not compile — the compiler will not guess.

void f(int);
void f(double);

f(3);      // int: exact
f(3.0);    // double: exact
f(3.0f);   // double: float to double is a promotion, float to int a conversion
f('a');    // int: char to int is a promotion
f(3L);     // error: long to int and long to double are both conversions — ambiguous

The last line is the one to remember. With the area set above, area(4) is ambiguous for the same reason: int to long long and int to double are both standard conversions of the same rank. Give the variable the exact type (long long side = 4;) or add an int overload, but do not rely on the compiler "obviously" preferring the integer one — it does not.

The string and pointer traps

void show(const std::string& s);
void show(bool b);

show("hello");     // calls show(bool)

"hello" is a const char[6]. Decaying to const char* and then to bool is a standard conversion; building a std::string is a user-defined one; the standard conversion wins. The cure is an overload that is exact for a literal — void show(const char* s) — or a std::string_view parameter, which a literal converts to cheaply (Module 5, lesson 6). The same rank rule makes f(0) prefer f(int) over f(const char*), and it is why nullptr exists: nullptr converts to any pointer and never to an integer, so f(nullptr) picks the pointer overload unambiguously where f(NULL) could not.

Signed and unsigned bite the same way: with g(int) and g(unsigned), g(v.size()) passes an unsigned long, whose conversions to unsigned and to int rank equally — ambiguous. Keep overload sets small and their parameter types far apart.

Default arguments

std::string frame(const std::string& text, char border = '*', int padding = 1);

frame("hi");            // * hi *
frame("hi", '#');       // # hi #
frame("hi", '#', 3);    // ### hi ###

A default is a value the compiler inserts at the call when the argument is missing. The rules:

  • Trailing only. Once a parameter has a default, every parameter after it must have one. f(int a = 1, int b) is an error, and there is no way to "skip" the middle argument at a call.
  • Specified once, on the first declaration the caller sees. Write it on the prototype; write the definition without it. Repeating it on the definition is an error ("default argument given for parameter 2 after previous specification"), and putting it only on a definition that comes after the calls means the calls never saw it.
  • Evaluated at each call, in the caller's scope. A default may be an expression — int retries = maxRetries() — and it is re-evaluated every time it is used.
  • Bound at compile time by the static type. With virtual functions (Module 10, lesson 2) the default from the base declaration is used even when the derived override runs — one of the reasons to keep defaults out of virtual functions.

A default is not part of the signature. frame(const std::string&, char, int) is the one function whether the caller supplied one, two or three arguments.

Overloading versus defaults

Use a default when the parameter is genuinely optional and the behaviour with and without it is the same function: a separator, a padding width, a log level. Use overloads when the parameter types differ, when the behaviours differ enough that one body would be full of if (hasValue), or when the "optional" argument changes the return type.

void log(const std::string& msg, int level = 1);               // one function, one behaviour
std::string describe(int v);
std::string describe(double v);                                // overloads: the type is the difference

When both would work, the default is simpler — one definition, one place to maintain — and the overload is more flexible. The wrong choice is neither; it is a single function with a flag parameter whose two halves share nothing.

What counts as a different signature

Differs inMakes an overload?
number of parametersyes
parameter types (int vs double, int& vs int)yes
const on what a pointer or reference names (const T& vs T&)yes
return typeno — error
parameter namesno — same function
top-level const on a by-value parameter (int vs const int)no — same function
default argumentsno — same function

Pitfalls

  • Ambiguity you did not expect. Mixed integer widths and mixed integer/floating overloads are the usual cause; the fix is an exact-type variable or an added overload, not a cast at every call.
  • show("literal") reaching a bool overload. Add a const char* overload or take std::string_view.
  • Default on the definition, prototype without it. Legal only if no call comes before the definition — and the day someone reorders the file, the calls stop compiling.
  • Overloads differing only in top-level const. void f(int) and void f(const int) are a redefinition error, not two functions.

Key takeaways

  • Overloads share a name and differ in parameter count or types; the return type never distinguishes them.
  • Resolution ranks exact match, promotion, standard conversion, then user-defined conversion; a tie is a compile error, and int versus long long/double overloads tie constantly.
  • A string literal is a pointer: it prefers bool over std::string. Use nullptr, not 0, for pointer overloads.
  • Default arguments are trailing, written once on the first declaration, and evaluated at every call.
  • Defaults for an optional value of the same kind; overloads when the types or behaviours differ.

Common questions

Why is my overloaded function call ambiguous?

Two overloads needed conversions of the same rank, so neither was better. With f(int) and f(double), f(3L) is ambiguous because long to int and long to double are both standard conversions. Pass an argument of the exact type, or add an overload for it.

What are default arguments in C++?

A default argument is a value the compiler inserts when a call leaves that argument out, as in std::string frame(const std::string& text, char border = '*');. Defaults must be trailing, are written once on the first declaration the caller sees rather than repeated on the definition, and are evaluated afresh at every call.

Can you overload a function by return type in C++?

No. Overloads are told apart only by their parameter lists, so int f() and double f() in the same scope is an error: a call f(); could not choose between them. Parameter names, default arguments and top-level const on a by-value parameter do not distinguish overloads either.

Why does a string literal call the bool overload instead of the string one?

A literal such as "hello" is a const char array; decaying to a pointer and converting to bool is a standard conversion, while building a std::string is a user-defined one, which ranks lower. So with show(const std::string&) and show(bool), show("hello") calls the bool one. Add a const char* overload or take std::string_view.

Should I use overloading or default arguments?

Use a default when the parameter is genuinely optional and the function behaves the same way with or without it, such as a separator or a padding width. Use overloads when the parameter types differ, when the behaviours differ, or when the optional argument would change the return type.

Mark as readSign in to keep your place.

C++ Function Overloading and Default Arguments Explained

C++ overloads share a name and differ in parameter types; the compiler ranks conversions to pick one. Ambiguous calls, string literals and default arguments.

  • Course: C++ study plan
  • Module: Functions
  • Kind: Lesson
  • Reading time: 13 min
  • Runtime: Clang 18 · C++20

What is function overloading in C++?

Function overloading lets several C++ functions share one name as long as their parameter lists differ in number or types, such as area(long long) and area(double). At each call the compiler ranks the viable overloads by conversion — exact match, promotion, standard conversion, then user-defined conversion — and picks the best. The return type never distinguishes overloads, and a tie is a compile error.

Lesson

Two functions may share a name if their parameter lists differ; a parameter may have a value the caller can leave out. Both features exist to make call sites read naturally — area(4), area(3, 5), frame("hi") — and both have rules that surprise people who have only seen them work. This lesson settles how the compiler picks an overload (and when it refuses to), the string and pointer cases that pick the wrong one, where a default argument may be written, and when each feature is the clearer choice.

What overloading is

long long area(long long side);                     // a square
long long area(long long width, long long height);  // a rectangle
double area(double radius);                         // a circle

Three functions, one name, told apart by their signatures — the parameter count and types. The return type plays no part: int f() and double f() in the same scope is an error, "ambiguating new declaration", because a call f(); could not choose between them.

Overloading exists so that one idea gets one name. std::to_string has nine overloads, std::abs has one per numeric type, std::sort has two — with and without a comparator. A good overload set does the same thing for every parameter list; a set whose members do different things is a naming problem dressed as a language feature.

How the compiler chooses

At a call, the compiler collects every function with that name that is visible (the candidates), discards the ones the arguments cannot be converted to (leaving the viable ones), and ranks the survivors by how good the conversion for each argument is:

  1. Exact match — the argument's type, or a trivial adjustment (adding const to what a reference names, array to pointer).
  2. Promotion — char, short, bool and small enums to int; float to double.
  3. Standard conversion — any other built-in conversion: int to double, double to int, long to int, integer to bool, a pointer to bool, nullptr to a pointer.
  4. User-defined conversion — a constructor or conversion operator, such as const char* to std::string.

The best viable function must be strictly better on at least one argument and no worse on the rest. If two tie, the call is ambiguous and does not compile — the compiler will not guess.

void f(int);
void f(double);

f(3);      // int: exact
f(3.0);    // double: exact
f(3.0f);   // double: float to double is a promotion, float to int a conversion
f('a');    // int: char to int is a promotion
f(3L);     // error: long to int and long to double are both conversions — ambiguous

The last line is the one to remember. With the area set above, area(4) is ambiguous for the same reason: int to long long and int to double are both standard conversions of the same rank. Give the variable the exact type (long long side = 4;) or add an int overload, but do not rely on the compiler "obviously" preferring the integer one — it does not.

The string and pointer traps

void show(const std::string& s);
void show(bool b);

show("hello");     // calls show(bool)

"hello" is a const char[6]. Decaying to const char* and then to bool is a standard conversion; building a std::string is a user-defined one; the standard conversion wins. The cure is an overload that is exact for a literal — void show(const char* s) — or a std::string_view parameter, which a literal converts to cheaply (Module 5, lesson 6). The same rank rule makes f(0) prefer f(int) over f(const char*), and it is why nullptr exists: nullptr converts to any pointer and never to an integer, so f(nullptr) picks the pointer overload unambiguously where f(NULL) could not.

Signed and unsigned bite the same way: with g(int) and g(unsigned), g(v.size()) passes an unsigned long, whose conversions to unsigned and to int rank equally — ambiguous. Keep overload sets small and their parameter types far apart.

Default arguments

std::string frame(const std::string& text, char border = '*', int padding = 1);

frame("hi");            // * hi *
frame("hi", '#');       // # hi #
frame("hi", '#', 3);    // ### hi ###

A default is a value the compiler inserts at the call when the argument is missing. The rules:

  • Trailing only. Once a parameter has a default, every parameter after it must have one. f(int a = 1, int b) is an error, and there is no way to "skip" the middle argument at a call.
  • Specified once, on the first declaration the caller sees. Write it on the prototype; write the definition without it. Repeating it on the definition is an error ("default argument given for parameter 2 after previous specification"), and putting it only on a definition that comes after the calls means the calls never saw it.
  • Evaluated at each call, in the caller's scope. A default may be an expression — int retries = maxRetries() — and it is re-evaluated every time it is used.
  • Bound at compile time by the static type. With virtual functions (Module 10, lesson 2) the default from the base declaration is used even when the derived override runs — one of the reasons to keep defaults out of virtual functions.

A default is not part of the signature. frame(const std::string&, char, int) is the one function whether the caller supplied one, two or three arguments.

Overloading versus defaults

Use a default when the parameter is genuinely optional and the behaviour with and without it is the same function: a separator, a padding width, a log level. Use overloads when the parameter types differ, when the behaviours differ enough that one body would be full of if (hasValue), or when the "optional" argument changes the return type.

void log(const std::string& msg, int level = 1);               // one function, one behaviour
std::string describe(int v);
std::string describe(double v);                                // overloads: the type is the difference

When both would work, the default is simpler — one definition, one place to maintain — and the overload is more flexible. The wrong choice is neither; it is a single function with a flag parameter whose two halves share nothing.

What counts as a different signature

Differs inMakes an overload?
number of parametersyes
parameter types (int vs double, int& vs int)yes
const on what a pointer or reference names (const T& vs T&)yes
return typeno — error
parameter namesno — same function
top-level const on a by-value parameter (int vs const int)no — same function
default argumentsno — same function

Pitfalls

  • Ambiguity you did not expect. Mixed integer widths and mixed integer/floating overloads are the usual cause; the fix is an exact-type variable or an added overload, not a cast at every call.
  • show("literal") reaching a bool overload. Add a const char* overload or take std::string_view.
  • Default on the definition, prototype without it. Legal only if no call comes before the definition — and the day someone reorders the file, the calls stop compiling.
  • Overloads differing only in top-level const. void f(int) and void f(const int) are a redefinition error, not two functions.

Key takeaways

  • Overloads share a name and differ in parameter count or types; the return type never distinguishes them.
  • Resolution ranks exact match, promotion, standard conversion, then user-defined conversion; a tie is a compile error, and int versus long long/double overloads tie constantly.
  • A string literal is a pointer: it prefers bool over std::string. Use nullptr, not 0, for pointer overloads.
  • Default arguments are trailing, written once on the first declaration, and evaluated at every call.
  • Defaults for an optional value of the same kind; overloads when the types or behaviours differ.

Common questions

Why is my overloaded function call ambiguous?

Two overloads needed conversions of the same rank, so neither was better. With f(int) and f(double), f(3L) is ambiguous because long to int and long to double are both standard conversions. Pass an argument of the exact type, or add an overload for it.

What are default arguments in C++?

A default argument is a value the compiler inserts when a call leaves that argument out, as in std::string frame(const std::string& text, char border = '*');. Defaults must be trailing, are written once on the first declaration the caller sees rather than repeated on the definition, and are evaluated afresh at every call.

Can you overload a function by return type in C++?

No. Overloads are told apart only by their parameter lists, so int f() and double f() in the same scope is an error: a call f(); could not choose between them. Parameter names, default arguments and top-level const on a by-value parameter do not distinguish overloads either.

Why does a string literal call the bool overload instead of the string one?

A literal such as "hello" is a const char array; decaying to a pointer and converting to bool is a standard conversion, while building a std::string is a user-defined one, which ranks lower. So with show(const std::string&) and show(bool), show("hello") calls the bool one. Add a const char* overload or take std::string_view.

Should I use overloading or default arguments?

Use a default when the parameter is genuinely optional and the function behaves the same way with or without it, such as a separator or a padding width. Use overloads when the parameter types differ, when the behaviours differ, or when the optional argument would change the return type.

Exercises

Three areas, one name

Write an overload set named area:

  • long long area(long long side); — a square.
  • long long area(long long width, long long height); — a rectangle.
  • double area(double radius); — a circle, π × r² using std::numbers::pi.

Read n, then n lines: square <side>, rect <width> <height> or circle <radius>. Print square: <area>, rect: <area> or circle: <area fixed to 2 decimals>.

Input: n, then n shape lines (integer sides up to 100 000; the radius is a decimal). Output: n lines.

Example: input 3, square 4, rect 3 5, circle 2.5 →

square: 16
rect: 15
circle: 19.63

Read the sides into long long variables and the radius into a double so every call is an exact match: area(4) with an int argument would be ambiguous between the long long and double overloads.

Frame it, with defaults

Write std::string frame(const std::string& text, char border = '*', int padding = 1); — it returns the border character repeated padding times, a space, the text, a space, and the border repeated again. Put the defaults on the prototype above main and write the definition below without them.

Read n, then n lines each holding text (one word) optionally followed by a border character and optionally a padding count. Call frame with one, two or three arguments — exactly as many as the line supplied — and print the result.

Input: n, then n lines. Output: n lines.

Example: input 3, hi, hi #, hi # 3 →

* hi *
# hi #
### hi ###

The starter reads each line into a std::istringstream; extracting the border and the padding fails cleanly when they are absent, which tells you how many arguments to pass.

In this module: Functions

← Passing parameters — by value, by reference, by const reference · Recursion — base cases, the call stack and memoisation →