7.8. Overloaded FunctionsTwo functions that appear in the same scope are overloaded if they have the same name but have different parameter lists. If you have written an arithmetic expression in a programming language, you have used an overloaded function. The expression 1 + 3 invokes the addition operation for integer operands, whereas the expression 1.0 + 3.0 invokes a different operation that adds floating-point operands. It is the compiler's responsibility, not the programmer's, to distinguish between the different operations and to apply the appropriate operation depending on the operands' types. Similarly, we may define a set of functions that perform the same general action but that apply to different parameter types. These functions may be called without worrying about which function is invoked, much as we can add ints or doubles without worrying whether integer arithmetic or floating-point arithmetic is performed. Function overloading can make programs easier to write and to understand by eliminating the need to inventand remembernames that exist only to help the compiler figure out which function to call. For example, a database application might well have several lookup functions that could do the lookup based on name, phone number, account number, and so on. Function overloading allows us to define a collection of functions, each named lookup, that differ in terms of what values they use to do the search. We can call lookup passing a value of any of several types: Record lookup(const Account&); // find by Account Record lookup(const Phone&); // find by Phone Record lookup(const Name&); // find by Name Record r1, r2; r1 = lookup(acct); // call version that takes an Account r2 = lookup(phone); // call version that takes a Phone Here, all three functions share the same name, yet they are three distinct functions. The compiler uses the argument type(s) passed in the call to figure out which function to call. To understand function overloading, we must understand how to define a set of overloaded functions and how the compiler decides which function to use for a given call. We'll review these topics in the remainder of this section.
Distinguishing Overloading from Redeclaring a FunctionIf the return type and parameter list of two functions declarations match exactly, then the second declaration is treated as a redeclaration of the first. If the parameter lists of two functions match exactly but the return types differ, then the second declaration is an error:
Record lookup(const Account&);
bool lookup(const Account&); // error: only return type is different
Functions cannot be overloaded based only on differences in the return type. Two parameter lists can be identical, even if they don't look the same: // each pair declares the same function Record lookup(const Account &acct); Record lookup(const Account&); // parameter names are ignored typedef Phone Telno; Record lookup(const Phone&); Record lookup(const Telno&); // Telno and Phone are the same type Record lookup(const Phone&, const Name&); // default argument doesn't change the number of parameters Record lookup(const Phone&, const Name& = ""); // const is irrelevent for nonreference parameters Record lookup(Phone); Record lookup(const Phone); // redeclaration In the first pair, the first declaration names its parameter. Parameter names are only a documentation aid. They do not change the parameter list. In the second pair, it looks like the types are different, but Telno is not a new type; it is a synonym for Phone. A typedef name provides an alternative name for an existing data type; it does not create a new data type. Therefore, two parameters that differ only in that one uses a typedef and the other uses the type to which the typedef corresponds are not different. In the third pair, the parameter lists differ only in their default arguments. A default argument doesn't change the number of parameters. The function takes two arguments, whether they are supplied by the user or by the compiler. The last pair differs only as to whether the parameter is const. This difference has no effect on the objects that can be passed to the function; the second declaration is treated as a redeclaration of the first. The reason follows from how arguments are passed. When the parameter is copied, whether the parameter is const is irrelevantthe function executes on a copy. Nothing the function does can change the argument. As a result, we can pass a const object to either a const or nonconst parameter. The two parameters are indistinguishable. It is worth noting that the equivalence between a parameter and a const parameter applies only to nonreference parameters. A function that takes a const reference is different from on that takes a nonconst reference. Similarly, a function that takes a pointer to a const type differs from a function that takes a pointer to the nonconst object of the same type.
7.8.1. Overloading and ScopeWe saw in the program on page 54 that scopes in C++ nest. A name declared local to a function hides the same name declared in the global scope (Section 2.3.6, p. 54). The same is true for function names as for variable names: /* Program for illustration purposes only: * It is bad style for a function to define a local variable * with the same name as a global name it wants to use */ string init(); // the name init has global scope void fcn() { int init = 0; // init is local and hides global init string s = init(); // error: global init is hidden } Normal scoping rules apply to names of overloaded functions. If we declare a function locally, that function hides rather than overloads the same function declared in an outer scope. As a consequence, declarations for every version of an overloaded function must appear in the same scope.
To explain how scope interacts with overloading we will violate this practice and use a local function declaration. As an example, consider the following program: void print(const string &); void print(double); // overloads the print function void fooBar(int ival) { void print(int); // new scope: hides previous instances of print print("Value: "); // error: print(const string &) is hidden print(ival); // ok: print(int) is visible print(3.14); // ok: calls print(int); print(double) is hidden } The declaration of print(int) in the function fooBar hides the other declarations of print. It is as if there is only one print function available: the one that takes a single int parameter. Any use of the name print at this scopeor a scope nested in this scopewill resolve to this instance. When we call print, the compiler first looks for a declaration of that name. It finds the local declaration for print that takes an int. Once the name is found, the compiler does no further checks to see if the name exists in an outer scope. Instead, the compiler assumes that this declaration is the one for the name we are using. What remains is to see if the use of the name is valid The first call passes a string literal but the function parameter is an int. A string literal cannot be implicitly converted to an int, so the call is an error. The print(const string&) function, which would have matched this call, is hidden and is not considered when resolving this call. When we call print passing a double, the process is repeated. The compiler finds the local definition of print(int). The double argument can be converted to an int, so the call is legal.
Had we declared print(int) in the same scope as the other print functions, then it would be another overloaded version of print. In that case, these calls would be resolved differently: void print(const string &); void print(double); // overloads print function void print(int); // another overloaded instance void fooBar2(int ival) { print("Value: "); // ok: calls print(const string &) print(ival); // ok: print(int) print(3.14); // ok: calls print (double) } Now when the compiler looks for the name print it finds three functions with that name. On each call it selects the version of print that matches the argument that is passed. 7.8.2. Function Matching and Argument ConversionsFunction overload resolution (also known as function matching) is the process by which a function call is associated with a specific function from a set of overloaded functions. The compiler matches a call to a function automatically by comparing the actual arguments used in the call with the parameters offered by each function in the overload set. There are three possible outcomes:
Most of the time it is straghtforward to determine whether a particular call is legal and if so, which function will be invoked by the compiler. Often the functions in the overload set differ in terms of the number of arguments, or the types of the arguments are unrelated. Function matching gets tricky when multiple functions have parameters that are related by conversions (Section 5.12, p. 178). In these cases, programmers need to have a good grasp of the process of function matching. 7.8.3. The Three Steps in Overload ResolutionConsider the following set of functions and function call: void f(); void f(int); void f(int, int); void f(double, double = 3.14); f(5.6); // calls void f(double, double) Candidate FunctionsThe first step of function overload resolution identifies the set of overloaded functions considered for the call. The functions in this set are the candidate functions. A candidate function is a function with the same name as the function that is called and for which a declaration is visible at the point of the call. In this example, there are four candidate functions named f. Determining the Viable FunctionsThe second step selects the functions from the set of candidate functions that can be called with the arguments specified in the call. The selected functions are the viable functions. To be viable, a function must meet two tests. First, the function must have the same number of parameters as there are arguments in the call. Second, the type of each argument must matchor be convertible tothe type of its corresponding parameter.
For the call f(5.6), we can eliminate two of our candidate functions because of a mismatch on number of arguments. The function that has no parameters and the one that has two int parameters are not viable for this call. Our call has only one argument, and these functions have zero and two parameters, respectively. On the other hand, the function that takes two doubles might be viable. A call to a function declaration that has a default argument (Section 7.4.1, p. 253) may omit that argument. The compiler will automatically supply the default argument value for the omitted argument. Hence, a given call might have more arguments than appear explicitly. Having used the number of arguments to winnow the potentially viable functions, we must now look at whether the argument types match those of the parameters. As with any call, an argument might match its parameter either because the types match exactly or because there is a conversion from the argument type to the type of the parameter. In the example, both of our remaining functions are viable.
Finding the Best Match, If AnyThe third step of function overload resolution determines which viable function has the best match for the actual arguments in the call. This process looks at each argument in the call and selects the viable function (or functions) for which the corresponding parameter best matches the argument. The details of "best" here will be explained in the next section, but the idea is that the closer the types of the argument and parameter are to each other, the better the match. So, for example, an exact type match is better than a match that requires a conversion from the argument type to the parameter type. In our case, we have only one explicit argument to consider. That argument has type double. To call f(int), the argument would have to be converted from double to int. The other viable function, f(double, double), is an exact match for this argument. Because an exact match is better than a match that requires a conversion, the compiler will resolve the call f(5.6) as a call to the function that has two double parameters. Overload Resolution with Multiple ParametersFunction matching is more complicated if there are two or more explicit arguments. Given the same functions named f, let's analyze the following call: f(42, 2.56); The set of viable functions is selected in the same way. The compiler selects those functions that have the required number of parameters and for which the argument types match the parameter types. In this case, the set of viable functions are f(int, int) and f(double, double). The compiler then determines argument by argument which function is (or functions are) the best match. There is a match if there is one and only one function for which
If after looking at each argument there is no single function that is preferable, then the call is in error. The compiler will complain that the call is ambiguous. In this call, when we look only at the first argument, we find that the function f(int, int) is an exact match. To match the second function, the int argument 42 must be converted to a double. A match through a built-in conversion is "less good" than one that is exact. So, considering only this parameter, the function that takes two ints is a better match than the function that takes two doubles. However, when we look at the second argument, then the function that takes two doubles is an exact match to the argument 2.56. Calling the version of f that takes two ints would require that 2.56 be converted from double to int. When we consider only the second parameter, then the function f(double, double) is the better match. This call is therefore ambiguous: Each viable function is a better match on one of the arguments to the call. The compiler will generate an error. We could force a match by explicitly casting one of our arguments: f(static_cast<double>(42), 2.56); // calls f(double, double) f(42, static_cast<int>(2.56)); // calls f(int, int)
7.8.4. Argument-Type ConversionsIn order to determine the best match, the compiler ranks the conversions that could be used to convert each argument to the type of its corresponding parameter. Conversions are ranked in descending order as follows:
These examples bear study to cement understanding both of function matching in particular and of the relationships among the built-in types in general. Matches Requiring Promotion or ConversionPromotions or conversions are applied when the type of the argument can be promoted or converted to the appropriate parameter type using one of the standard conversions. One important point to realize is that the small integral types promote to int. Given two functions, one of which takes an int and the other a short, the int version will be a better match for a value of any integral type other than short, even though short might appear on the surface to be a better match: void ff(int); void ff(short); ff('a'); // char promotes to int, so matches f(int) A character literal is type char, and chars are promoted to int. That promoted type matches the type of the parameter of function ff(int). A char could also be converted to short, but a conversion is a "less good" match than a promotion. And so this call will be resolved as a call to ff (int). A conversion that is done through a promotion is preferred to another standard conversion. So, for example, a char is a better match for a function taking an int than it is for a function taking a double. All other standard conversions are treated as equivalent. The conversion from char to unsigned char, for example, does not take precedence over the conversion from char to double. As a concrete example, consider:
extern void manip(long);
extern void manip(float);
manip(3.14); // error: ambiguous call
The literal constant 3.14 is a double. That type could be converted to either long or float. Because there are two possible standard conversions, the call is ambiguous. No one standard conversion is given precedence over another. Parameter Matching and EnumerationsRecall that an object of enum type may be initialized only by another object of that enum type or one of its enumerators (Section 2.7, p. 63). An integral object that happens to have the same value as an enumerator cannot be used to call a function expecting an enum argument: enum Tokens {INLINE = 128, VIRTUAL = 129}; void ff(Tokens); void ff(int); int main() { Tokens curTok = INLINE; ff(128); // exactly matches ff(int) ff(INLINE); // exactly matches ff(Tokens) ff(curTok); // exactly matches ff(Tokens) return 0; } The call that passes the literal 128 matches the version of ff that takes an int. Although we cannot pass an integral value to a enum parameter, we can pass an enum to a parameter of integral type. When we do so, the enum value promotes to int or to a larger integral type. The actual promotion type depends on the values of the enumerators. If the function is overloaded, the type to which the enum promotes determines which function is called: void newf(unsigned char); void newf(int); unsigned char uc = 129; newf(VIRTUAL); // calls newf(int) newf(uc); // calls newf(unsigned char) The enum Tokens has only two enumerators, the largest of which has a value of 129. That value can be represented by the type unsigned char, and many compilers would store the enum as an unsigned char. However, the type of VIRTUAL is not unsigned char. Enumerators and values of an enum type, are not promoted to unsigned char, even if the values of the enumerators would fit.
Overloading and const Parameters
We can overload a function based on whether a reference parameter refers to a const or nonconst type. Overloading on const for a reference parameter is valid because the compiler can use whether the argument is const to determine which function to call: Record lookup(Account&); Record lookup(const Account&); // new function const Account a(0); Account b; lookup(a); // calls lookup(const Account&) lookup(b); // calls lookup(Account&) If the parameter is a plain reference, then we may not pass a const object for that parameter. If we pass a const object, then the only function that is viable is the version that takes a const reference. When we pass a nonconst object, either function is viable. We can use a nonconst object to initializer either a const or nonconst reference. However, initializing a const reference to a nonconst object requires a conversion, whereas initializing a nonconst parameter is an exact match. Pointer parameters work in a similar way. We may pass the address of a const object only to a function that takes a pointer to const. We may pass a pointer to a nonconst object to a function taking a pointer to a const or nonconst type. If two functions differ only as to whether a pointer parameter points to const or nonconst, then the parameter that points to the nonconst type is a better match for a pointer to a nonconst object. Again, the compiler can distinguish: If the argument is const, it calls the function that takes a const*; otherwise, if the argument is a nonconst, the function taking a plain pointer is called. It is worth noting that we cannot overload based on whether the pointer itself is const:
f(int *);
f(int *const); // redeclaration
Here the const applies to the pointer, not the type to which the pointer points. In both cases the pointer is copied; it makes no difference whether the pointer itself is const. As we noted on page 267, when a parameter is passed as a copy, we cannot overload based on whether that parameter is const.
|