Free Hosting

Function overloading


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 nX, int nY)
{
    return nX + nY;
}
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 AddI(int nX, int nY)
{
    return nX + nY;
}
 
double AddD(double dX, double dY)
{
    return dX + dY;
}
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 (calling AddD() with integer parameters may produce the wrong result due to precision issues).
Function overloading provides a better solution. Using function overloading, we can declare another Add() function that takes double parameters:
1
2
3
4
double Add(double dX, double dY)
{
    return dX + dY;
}
We now have two version of Add():
1
2
int Add(int nX, int nY); // integer version
double Add(double dX, double dY); // floating point version
Which version of Add() gets called depends on the arguments used in the 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 nX, int nY, int nZ)
{
    return nX + nY + nZ;
}
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.
Note that the function’s return type is NOT considered when overloading functions. 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();
But 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. Consequently, these functions will need to be given different names.
Also keep in mind that declaring a typedef does not introduce a new type — consequently, the following the two declarations of Print() are considered identical:
1
2
3
typedef char *string;
void Print(string szValue);
void Print(char *szValue);
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 *szValue);
void Print(int nValue);
 
Print(0); // exact match with Print(int)
Although 0 could technically match Print(char*), it exactly matches Print(int). Thus Print(int) is the best match available.
2) If no exact match is found, C++ tries to find a match through promotion. In the lesson on type conversion and casting, 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 *szValue);
void Print(int nValue);
 
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 found, C++ tries to find a match through standard conversion. Standard conversions include:
  • Any numeric type will match any other numeric type, including unsigned (eg. int to float)
  • Enum will match the formal type of a numeric type (eg. enum to float)
  • Zero will match a pointer type and numeric type (eg. 0 to char*, or 0 to float)
  • A pointer will match a void pointer
For example:
1
2
3
4
void Print(float fValue);
void Print(struct sValue);
 
Print('a'); // promoted to match Print(float)
In this case, because there is no Print(char), and no Print(int), 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 fValue);
void Print(int nValue);
 
X cValue; // declare a variable named cValue of type class X
Print(cValue); // cValue will be converted to an int and matched to Print(int)
Although cValue is of type class X, because this particular class has a user-defined conversion to int, the function call Print(cValue) 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 nValue);
void Print(float fValue);
 
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 two 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 parameter(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
Print(static_cast<unsigned int>(0)); // will call Print(unsigned int)
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).
Conclusion
Function overloading can lower a programs 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.

0 comments:

Blogger Template by Clairvo