Section 7.8. Overloaded Functions


7.8. Overloaded Functions

Two 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.

There may be only one instance of main in any program. The main function may not be overloaded.



Distinguishing Overloading from Redeclaring a Function

If 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.

Advice: When Not to Overload a Function Name

Although overloading can be useful in avoiding the necessity to invent (and remember) names for common operations, it is easy to take this advantage too far. There are some cases where providing different function names adds information that makes the program easier to understand. Consider a set of member functions for a Screen class that move Screen's cursor.

      Screen& moveHome();      Screen& moveAbs(int, int);      Screen& moveRel(int, int, char *direction); 

It might at first seem better to overload this set of functions under the name move:

      Screen& move();      Screen& move(int, int);      Screen& move(int, int, *direction); 

However, by overloading these functions we've lost information that was inherent in the function names and by doing so may have rendered the program more obscure.

Although cursor movement is a general operation shared by all these functions, the specific nature of that movement is unique to each of these functions. moveHome, for example, represents a special instance of cursor movement. Which of the two calls is easier to understand for a reader of the program? Which of the two calls is easier to remember for a programmer using the Screen class?

      // which is easier to understand?      myScreen.home(); // we think this one!      myScreen.move(); 


7.8.1. Overloading and Scope

We 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.

In general, it is a bad idea to declare a function locally. Function declarations should go in header files.


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.

In C++ name lookup happens before type checking.



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 Conversions

Function 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:

  1. The compiler finds one function that is a best match for the actual arguments and generates code to call that function.

  2. There is no function with parameters that match the arguments in the call, in which case the compiler indicates a compile-time error.

  3. There is more than one function that matches and none of the matches is clearly best. This case is also an error; the call is ambiguous.

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.

Exercises Section 7.8.1

Exercise 7.34:

Define a set of overloaded functions named error that would match the following calls:

      int index, upperBound;      char selectVal;      // ...      error("Subscript out of bounds: ", index, upperBound);      error("Division by zero");      error("Invalid selection", selectVal); 

Exercise 7.35:

Explain the effect of the second declaration in each one of the following sets of declarations. Indicate which, if any, are illegal.

      (a) int calc(int, int);          int calc(const int, const int);      (b) int get();          double get();      (c) int *reset(int *);          double *reset(double *); 


7.8.3. The Three Steps in Overload Resolution

Consider 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 Functions

The 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 Functions

The 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.

When a function has default arguments (Section 7.4.1, p. 253), a call may appear to have fewer arguments than it actually does. Default arguments are arguments and are treated the same way as any other argument during function matching.



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.

  • f(int) is a viable function because a conversion exists that can convert the argument of type double to the parameter of type int.

  • f(double, double) is a viable function because a default argument is provided for the function's second parameter and its first parameter is of type double, which exactly matches the type of the parameter.

If there are no viable functions, then the call is in error.



Finding the Best Match, If Any

The 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 Parameters

Function 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

  1. The match for each argument is no worse than the match required by any other viable function.

  2. There is at least one argument for which the match is better than the match provided by any other viable function.

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) 

In practice, arguments should not need casts when calling over-loaded functions: The need for a cast means that the parameter sets are designed poorly.



7.8.4. Argument-Type Conversions

In 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:

Exercises Section 7.8.3

Exercise 7.36:

What is a candidate function? What is a viable function?

Exercise 7.37:

Given the declarations for f, determine whether the following calls are legal. For each call list the viable functions, if any. If the call is illegal, indicate whether there is no match or why the call is ambiguous. If the call is legal, indicate which function is the best match.

      (a) f(2.56, 42);      (b) f(42);      (c) f(42, 0);      (d) f(2.56, 3.14); 


  1. An exact match. The argument and parameter types are the same.

  2. Match through a promotion (Section 5.12.2, p. 180).

  3. Match through a standard conversion (Section 5.12.3, p. 181).

  4. Match through a class-type conversion. (Section 14.9 (p. 535) covers these conversions.)

Promotions and conversions among the built-in types can yield surprising results in the context of function matching. Fortunately, well-designed systems rarely include functions with parameters as closely related as those in the following examples.



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 Conversion

Promotions 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 Enumerations

Recall 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.

When using overloaded functions with enum parameters, remember: Two enumeration types may behave quite differently during function overload resolution, depending on the value of their enumeration constants. The enumerators determine the type to which they promote. And that type is machine-dependent.



Overloading and const Parameters

Whether a parameter is const only matters when the parameter is a reference or pointer.



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.

Exercises Section 7.8.4

Exercise 7.38:

Given the following declarations,

      void manip(int, int);      double dobj; 

what is the rank (Section 7.8.4, p. 272) of each conversion in the following calls?

      (a) manip('a', 'z');    (b) manip(55.4, dobj); 

Exercise 7.39:

Explain the effect of the second declaration in each one of the following sets of declarations. Indicate which, if any, are illegal.

      (a) int calc(int, int);          int calc(const int&, const int&);      (b) int calc(char*, char*);          int calc(const char*, const char*);      (c) int calc(char*, char*);          int calc(char* const, char* const); 

Exercise 7.40:

Is the following function call legal? If not, why is the call in error?

      enum Stat { Fail, Pass };      void test(Stat);      test(0); 




C++ Primer
C Primer Plus (5th Edition)
ISBN: 0672326965
EAN: 2147483647
Year: 2006
Pages: 223
Authors: Stephen Prata

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net