Function overloading is a feature of C++ that allows us to create multiple functions with the same name, so long as they have different parameters. Consider the following function:
1 2 3 4 |
int add(int x, int y) { return x + y; } |
This trivial function adds two integers. However, what if we also need to add two floating point numbers? This function is not at all suitable, as any floating point parameters would be converted to integers, causing the floating point arguments to lose their fractional values.
One way to work around this issue is to define multiple functions with slightly different names:
1 2 3 4 5 6 7 8 9 |
int addInteger(int x, int y) { return x + y; } double addDouble(double x, double y) { return x + y; } |
However, for best effect, this requires that you define a consistent naming standard, remember the name of all the different flavors of the function, and call the correct one.
Function overloading provides a better solution. Using function overloading, we can simply declare another add() function that takes double parameters:
1 2 3 4 |
double add(double x, double y) { return x + y; } |
We now have two versions of add():
1 2 |
int add(int x, int y); // integer version double add(double x, double y); // floating point version |
Although you might expect this to cause a naming conflict, that is not the case here. The compiler is able to determine which version of add() to call based on the arguments used in the function call. If we provide two ints, C++ will know we mean to call add(int, int). If we provide two floating point numbers, C++ will know we mean to call add(double, double). In fact, we can define as many overloaded add() functions as we want, so long as each add() function has unique parameters.
Consequently, it’s also possible to define add() functions with a differing number of parameters:
1 2 3 4 |
int add(int x, int y, int z) { return x + y + z; } |
Even though this add() function has 3 parameters instead of 2, because the parameters are different than any other version of add(), this is valid.
Function return types are not considered for uniqueness
A function’s return type is NOT considered when overloading functions. (Note for advanced readers: This was an intentional choice, as it ensures the behavior of a function call or subexpression can be determined independently from the rest of the expression, making understanding complex expressions much simpler. Put another way, we can always determine which version of a function will be called based solely on the arguments. If return values were included, then we wouldn’t have an easy syntactic way to tell which version of a function was being called -- we’d also have to understand how the return value was being used, which requires a lot more analysis).
Consider the case where you want to write a function that returns a random number, but you need a version that will return an int, and another version that will return a double. You might be tempted to do this:
1 2 |
int getRandomValue(); double getRandomValue(); |
The compiler will flag this as an error. These two functions have the same parameters (none), and consequently, the second getRandomValue() will be treated as an erroneous redeclaration of the first.
The best way to address this is to give the functions different names:
1 2 |
int getRandomInt(); double getRandomDouble(); |
An alternative method is to make the functions return void, and have the return value passed back to the caller as an out parameter (see lesson 10.3 -- Passing arguments by reference if you need a reminder what an out parameter is).
1 2 |
void getRandomValue(int &out); void getRandomValue(double &out); |
Because these functions have different parameters, they are considered unique. However, there are downsides to doing this. First, the syntax is awkward, and you can’t route the output of this function directly into the input of another. Consider:
1 2 3 4 5 6 7 |
// method 1: getRandomInt() returns an int printValue(getRandomInt()); // easy // method 2: getRandomValue() has an int out parameter int temp; // now we need a temp variable getRandomValue(temp); // to call getRandomValue(int) here printValue(temp); // this has to be a separate line since getRandomValue() returns void |
Also, the type of the argument passed in must match the type of the parameter exactly. For these reasons, we don’t recommend this method.
Typedefs are not distinct
Since declaring a typedef does not introduce a new type, the following two declarations of print() are considered identical:
1 2 3 |
typedef char* mystring; void print(mystring value); void print(char* value); |
How function calls are matched with overloaded functions
Making a call to an overloaded function results in one of three possible outcomes:
1) A match is found. The call is resolved to a particular overloaded function.
2) No match is found. The arguments can not be matched to any overloaded function.
3) An ambiguous match is found. The arguments matched more than one overloaded function.
When an overloaded function is called, C++ goes through the following process to determine which version of the function will be called:
1) First, C++ tries to find an exact match. This is the case where the actual argument exactly matches the parameter type of one of the overloaded functions. For example:
1 2 3 4 |
void print(char *value); void print(int value); print(0); // exact match with print(int) |
Although 0 could technically match print(char*) (as a null pointer), it exactly matches print(int) (matching char* would require an implicit conversion). Thus print(int) is the best match available.
2) If no exact match is found, C++ tries to find a match through promotion. In a previous lesson, we covered how certain types can be automatically promoted via internal type conversion to other types. To summarize,
- Char, unsigned char, and short is promoted to an int.
- Unsigned short can be promoted to int or unsigned int, depending on the size of an int
- Float is promoted to double
- Enum is promoted to int
For example:
1 2 3 4 |
void print(char *value); void print(int value); print('a'); // promoted to match print(int) |
In this case, because there is no print(char), the char ‘a’ is promoted to an integer, which then matches print(int).
3) If no promotion is possible, C++ tries to find a match through standard conversion. Standard conversions include:
- Any numeric type will match any other numeric type, including unsigned (e.g. int to float)
- Enum will match the formal type of a numeric type (e.g. enum to float)
- Zero will match a pointer type and numeric type (e.g. 0 to char*, or 0 to float)
- A pointer will match a void pointer
For example:
1 2 3 4 5 |
struct Employee; // defined somewhere else void print(float value); void print(Employee value); print('a'); // 'a' converted to match print(float) |
In this case, because there is no print(char) (exact match), and no print(int) (promotion match), the ‘a’ is converted to a float and matched with print(float).
Note that all standard conversions are considered equal. No standard conversion is considered better than any of the others.
4) Finally, C++ tries to find a match through user-defined conversion. Although we have not covered classes yet, classes (which are similar to structs) can define conversions to other types that can be implicitly applied to objects of that class. For example, we might define a class X and a user-defined conversion to int.
1 2 3 4 5 6 7 |
class X; // with user-defined conversion to int void print(float value); void print(int value); X value{}; // declare a variable named value of type class X print(value); // value will be converted to an int and matched to print(int) |
Although value is of type class X, because this particular class has a user-defined conversion to int, the function call print(value) will resolve to the print(int) version of the function.
We will cover the details on how to do user-defined conversions of classes when we cover classes.
Ambiguous matches
If every overloaded function has to have unique parameters, how is it possible that a call could result in more than one match? Because all standard conversions are considered equal, and all user-defined conversions are considered equal, if a function call matches multiple candidates via standard conversion or user-defined conversion, an ambiguous match will result. For example:
1 2 3 4 5 6 |
void print(unsigned int value); void print(float value); print('a'); print(0); print(3.14159); |
In the case of print('a')
, C++ can not find an exact match. It tries promoting ‘a’ to an int, but there is no print(int) either. Using a standard conversion, it can convert ‘a’ to both an unsigned int and a floating point value. Because all standard conversions are considered equal, this is an ambiguous match.
print(0)
is similar. 0 is an int, and there is no print(int). It matches both calls via standard conversion.
print(3.14159)
might be a little surprising, as most programmers would assume it matches print(float). But remember that all literal floating point values are doubles unless they have the ‘f’ suffix. 3.14159 is a double, and there is no print(double). Consequently, it matches both calls via standard conversion.
Ambiguous matches are considered a compile-time error. Consequently, an ambiguous match needs to be disambiguated before your program will compile. There are a few ways to resolve ambiguous matches:
1) Often, the best way is simply to define a new overloaded function that takes parameters of exactly the type you are trying to call the function with. Then C++ will be able to find an exact match for the function call.
2) Alternatively, explicitly cast the ambiguous argument(s) to the type of the function you want to call. For example, to have print(0) call the print(unsigned int), you would do this:
1 2 |
int x{ 0 }; print(static_cast<unsigned int>(x)); // will call print(unsigned int) |
3) If your argument is a literal, you can use the literal suffix to ensure your literal is interpreted as the correct type:
1 |
print(0u); // will call print(unsigned int) since 'u' suffix is unsigned int |
The list of the most used suffixes can be found in lesson 4.13 -- Literals.
Matching for functions with multiple arguments
If there are multiple arguments, C++ applies the matching rules to each argument in turn. The function chosen is the one for which each argument matches at least as well as all the other functions, with at least one argument matching better than all the other functions. In other words, the function chosen must provide a better match than all the other candidate functions for at least one parameter, and no worse for all of the other parameters.
In the case that such a function is found, it is clearly and unambiguously the best choice. If no such function can be found, the call will be considered ambiguous (or a non-match).
For example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
#include <iostream> void fcn(char c, int x) { std::cout << 'a'; } void fcn(char c, double x) { std::cout << 'b'; } void fcn(char c, float x) { std::cout << 'c'; } int main() { fcn('x', 4); } |
In the above program, all functions match the first argument exactly. However, the top function matches the second parameter exactly, whereas the other functions require a conversion. Therefore, the top function (the one that prints ‘a’) is unambiguously the best match.
Conclusion
Function overloading can lower a program’s complexity significantly while introducing very little additional risk. Although this particular lesson is long and may seem somewhat complex (particularly the matching rules), in reality function overloading typically works transparently and without any issues. The compiler will flag all ambiguous cases, and they can generally be easily resolved.
Rule
Use function overloading to make your program simpler.
![]() |
![]() |
![]() |
Hi,
In the section "Typedefs are not distinct", how does the compiler differentiate between the type string that in the header <string> and the alias "string" that refers to char* ????
The string in the string header is part of the std:: namespace, whereas the string that refers to char* isn't.
In general, string will refer to the char* typedef, and std::string will refer to std::string. There's one exception to this (code in a namespace will "prefer" the type defined in the namespace to the global type) but it shouldn't matter (because you should never be defining code inside namespace named "std")
I Got it, Thanks for This Big Effort.
Hi Alex and Nascardiver, so using the keyword "auto" is a good candidate here?
Because the compiler will not look at the return type of the function, right? It only scans for the function parameters.
Hi kio!
`auto` return types should be avoided. Anyone who wants to use your function has to read its definition to know what the return type is. There are cases where `auto` return types can reasonably be used, but they're rare.
If you want to have an `add()` function for all types, templates are a better candidate than `auto`.
Hi Nascardriver, you are right about the "auto" keyword. I was looking about templates last night, I'm looking forward to reaching the chapter where we are going to tackle it!
Thanks for your time and response.
> "2) If no exact match is found, C++ tries to find a match through promotion. In lesson a previous lesson, we covered how certain types can be automatically promoted via internal type conversion to other types. To summarize,"
---> The second line, should be "In a previous lesson,"
Thank you :)
Three small suggestions:
..., the following two declarations of print() are considered identical:
instead of
..., the following two declarations of Print() are considered identical:
In lesson 6.15 — Implicit type conversion (coercion), ...
instead of
In lesson 4.4 -- type conversion and casting, ...
..., the function call print(value) will resolve to the print(int) version of the function.
instead of
..., the function call print(value) will resolve to the Print(int) version of the function.
but even if its a double, it sure seems like its closer to a float than an unsigned integer. can it not do a standard conversion from double to float and match print(float) ?
To steal a phrase from Andreas Krug, shouldn't the note for advanced readers be in a beautiful gray box?
Just a suggestion: you could replace the typedef with a more modern "using type alias". The syntax to understand to whom the asterisk actually belongs to is quite hard to remember correctly when dealing with typedefs and pointers
How about overloading like this?
Is this allowed?
I guess if it is allowed,
is equal with
?
It is not allowed. `const` is removed from value parameters during function lookup, so your 2 `myfunction` are the same.
It looks like i made a mistake, rather than
it was supposed to be something like this
Yes this is allowed, those are different functions. I don't understand what you're doing in your second example though.
>> We now have two version of add():
looks like you missed an s at the end of "version" there
also the rule at the end should be put in it's proper home, the beautiful green box
>In this case, because there is no print(char), the char ‘a’ is promoted to an integer, which then matches print(int).
Isn't print(char *value) taken here, since we're passing a char?
A `char` cannot be converted to a `char*`, but `char` is an integral type, it can be converted to `int`.
Hi,
I was wondering why I get the following compile error:
2 overloads have similar conversions
For each possible function, there'd have to be 1 conversion. Either from int to double or from double to int. No overload is a better match than the other. Use double literals when you use doubles.
So compiler is confused which version of the function is to choose because both versions require only one conversion of the arguement to fit?
Indeed. If neither function is a better match than the other, the compiler is helpless. You can explicitly tell the compiler which overload to call by performing a cast on the function pointer, but that's only a last resort if the clash is in a library and you're unable to update your functions. If such a clash occurs in your code, update the functions to resolve it (eg. by renaming one).
Thanks. It made sense now.
It looks awful for me. Implicit conversions is what I hate the most in programming by now. C++ is strongly typed. This is good. Let it be that way.
Implicit conversion is possible unwanted magic behind the scene. You call a function and the compiler chooses the best match, possibly not the one you think (different behavior) at the call moment ...
Better use templates. At least there you tell the compiler you want the same behavior for all types. Aka the function you think at call moment.
Should this
be
or am I just not understanding it? If you make char and a pointer to a string equal using a typedef, does that also make a pointer to a char and a regular string equal too?
It's correct as written. They placement of the asterisk might confuse you
How is it decided as to whether promote or convert a data type to another?
For eg, char value is 'promoted' to int, but it is 'converted' to unsigned int. I am having a difficult time remembering all these data types either being promoted or converted to another form. Can you please help?
I am particularly confused due to the following lines.
void print(unsigned int value);
void print(float value);
print('a');
print(0);
print(3.14159);
In the case of print('a'), C++ can not find an exact match. It tries promoting ‘a’ to an int, but there is no print(int) either. Using a standard conversion, it can convert ‘a’ to both an unsigned int and a floating point value.
This is discussed in lesson 6.15.
1. I wanted to check that you still need separate function declarations when the parameters are different but return type is the same.
2. For small functions, is this the right way to organise files? With separate .h and .cpp? Or would you put the definitions in the .h file?
Functions.h
Functions.cpp
- Pass/return non-fundamental types by reference.
- Don't `#include <Windows.h>`. Your code won't work on other OSs.
- Include as few headers as possible in headers. `Functions.h` doesn't need <iostream>.
Yes that's how you'd separate it. Don't define functions in headers (There are exceptions about which you'll learn later).
Do I #include <iostream> in Functions.cpp then? For some reason I thought that was wrong. Maybe I'm mixing that up with including.cpp files themselves, which I'm pretty sure is wrong.
> Do I #include <iostream> in Functions.cpp then?
Yes. "Functions.cpp" is the file using functionality from <iostream>, so it should be including it.
So, just to be clear, if a .h file requires std::<string>, you still #include <string> in the .h file, but not in the corresponding .cpp, due to contents being copied over?
Correct. In some situations I'll still include <string> in the .cpp file, even if I already have access through the header. Do what works best for you.
Gotcha, thanks very much!
Hi!
Typedefs are not distinct
Since declaring a typedef does not introduce a new type, the following two declarations of Print() are considered identical:
typedef char *string;
void print(string value);
void print(char *value);
I do not understand the use of '*' here. Please explain.
Thanks :)
It's a pointer/[char array]
Lesson 6.6 to 6.8a
typedef char *string is same as using *string = char.
so can we write
void print(*string value) = void print(char value) ?
I do not understand how the parameters 'string value' and 'char *value' are equal.
Alright.
Is it a valid assignment, if a string is passed as an argument to the function: void print(char *value)?
Yes, `string` is the same as `char*`.
Thank you so much. I am sure, to the beginners char* is frightening.
If they read the previous lessons it shouldn't be frightening since the author explains in the linked lessons what pointers are, and why C strings are stored as (char *).
thanks alot nas...
hi Alex,
i've seen some using function-like macros and deprecating function overloading
like this:
is this advisable anyway?
...ready to stick to your rules/recommendations always...
Quoting lesson 2.10:
"Function-like macros act like functions, and serve a similar purpose. We will not discuss them here, because their use is generally considered dangerous, and almost anything they can do can be done by a normal function."
And if you need that functionality, you should look into templates.