Team LiB
Previous Section Next Section

6.4. Overloaded Functions

 
Image

Functions that have the same name but different parameter lists and that appear in the same scope are overloaded. For example, in § 6.2.4 (p. 214) we defined several functions named print:

 

 

void print(const char *cp);
void print(const int *beg, const int *end);
void print(const int ia[], size_t size);

 

These functions perform the same general action but apply to different parameter types. When we call these functions, the compiler can deduce which function we want based on the argument type we pass:

 

 

int j[2] = {0,1};
print("Hello World");        // calls print(const char*)
print(j, end(j) - begin(j)); // calls print(const int*, size_t)
print(begin(j), end(j));     // calls print(const int*, const int*)

 

Function overloading eliminates the need to invent—and remember—names that exist only to help the compiler figure out which function to call.

 

Image Note

The main function may not be overloaded.

 

 

Defining Overloaded Functions

 

Consider a database application with several functions to find a record based on name, phone number, account number, and so on. Function overloading lets us define a collection of functions, each named lookup, that differ in terms of how they 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

Account acct;
Phone phone;
Record r1 = lookup(acct);  // call version that takes an Account
Record 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) to figure out which function to call.

 

Overloaded functions must differ in the number or the type(s) of their parameters. Each of the functions above takes a single parameter, but the parameters have different types.

 

It is an error for two functions to differ only in terms of their return types. If the parameter lists of two functions match but the return types differ, then the second declaration is an error:

 

 

Record lookup(const Account&);
bool lookup(const Account&);   // error: only the return type is different

 

Determining Whether Two Parameter Types Differ

 

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

 

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 type alias (§ 2.5.1, p. 67) provides an alternative name for an existing type; it does not create a new type. Therefore, two parameters that differ only in that one uses an alias and the other uses the type to which the alias corresponds are not different.

 

Overloading and const Parameters

 
Image

As we saw in § 6.2.3 (p. 212), top-level const2.4.3, p. 63) has no effect on the objects that can be passed to the function. A parameter that has a top-level const is indistinguishable from one without a top-level const:

 

 

Record lookup(Phone);
Record lookup(const Phone);   // redeclares Record lookup(Phone)

Record lookup(Phone*);
Record lookup(Phone* const);  // redeclares Record lookup(Phone*)

 

In these declarations, the second declaration declares the same function as the first.

 

On the other hand, we can overload based on whether the parameter is a reference (or pointer) to the const or nonconst version of a given type; such consts are low-level:

 

 

// functions taking const and nonconst references or pointers have different parameters
// declarations for four independent, overloaded functions
Record lookup(Account&);       // function that takes a reference to Account
Record lookup(const Account&); // new function that takes a const reference

Record lookup(Account*);       // new function, takes a pointer to Account
Record lookup(const Account*); // new function, takes a pointer to const

 

In these cases, the compiler can use the constness of the argument to distinguish which function to call. Because there is no conversion (§ 4.11.2, p. 162) from const, we can pass a const object (or a pointer to const) only to the version with a const parameter. Because there is a conversion to const, we can call either function on a nonconst object or a pointer to nonconst. However, as we’ll see in § 6.6.1 (p. 246), the compiler will prefer the nonconst versions when we pass a nonconst object or pointer to nonconst.

 

const_cast and Overloading

 

In § 4.11.3 (p. 163) we noted that const_casts are most useful in the context of overloaded functions. As one example, recall our shorterString function from § 6.3.2 (p. 224):

 

 

// return a reference to the shorter of two strings
const string &shorterString(const string &s1, const string &s2)
{
    return s1.size() <= s2.size() ? s1 : s2;
}

 

Advice: When Not to Overload a Function Name

Although overloading lets us avoid having to invent (and remember) names for common operations, we should only overload operations that actually do similar things. There are some cases where providing different function names adds information that makes the program easier to understand. Consider a set of functions that move the cursor on a Screen.

 

 

Screen& moveHome();
Screen& moveAbs(int, int);
Screen& moveRel(int, int, string 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, string direction);

 

However, by overloading these functions, we’ve lost information that was inherent in the function names. 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. Whether to overload these functions depends on which of these two calls is easier to understand:

 

 

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

 

 

This function takes and returns references to const string. We can call the function on a pair of nonconst string arguments, but we’ll get a reference to a const string as the result. We might want to have a version of shorterString that, when given nonconst arguments, would yield a plain reference. We can write this version of our function using a const_cast:

 

 

string &shorterString(string &s1, string &s2)
{
    auto &r = shorterString(const_cast<const string&>(s1),
                            const_cast<const string&>(s2));
    return const_cast<string&>(r);
}

 

This version calls the const version of shorterString by casting its arguments to references to const. That function returns a reference to a const string, which we know is bound to one of our original, nonconst arguments. Therefore, we know it is safe to cast that string back to a plain string& in the return.

 

Calling an Overloaded Function

 

Once we have defined a set of overloaded functions, we need to be able to call them with appropriate arguments. Function matching (also known as overload resolution) is the process by which a particular function call is associated with a specific function from a set of overloaded functions. The compiler determines which function to call by comparing the arguments in the call with the parameters offered by each function in the overload set.

 

In many—probably most—cases, it is straightforward for a programmer to determine whether a particular call is legal and, if so, which function will be called. Often the functions in the overload set differ in terms of the number of arguments, or the types of the arguments are unrelated. In such cases, it is easy to determine which function is called. Determining which function is called when the overloaded functions have the same number of parameters and those parameters are related by conversions (§ 4.11, p. 159) can be less obvious. We’ll look at how the compiler resolves calls involving conversions in § 6.6 (p. 242).

 

For now, what’s important to realize is that for any given call to an overloaded function, there are three possible outcomes:

 

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

 

• There is no function with parameters that match the arguments in the call, in which case the compiler issues an error message that there was no match.

 

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

 

Exercises Section 6.4

 

Exercise 6.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 get();
double get();

 

(c) int *reset(int *);
double *reset(double *);

 

 

6.4.1. Overloading and Scope

 
Image

Image Warning

Ordinarily, it is a bad idea to declare a function locally. However, to explain how scope interacts with overloading, we will violate this practice and use local function declarations.

 

 

Programmers new to C++ are often confused about the interaction between scope and overloading. However, overloading has no special properties with respect to scope: As usual, if we declare a name in an inner scope, that name hides uses of that name declared in an outer scope. Names do not overload across scopes:

 

 

string read();
void print(const string &);
void print(double);   // overloads the print function
void fooBar(int ival)
{
    bool read = false; // new scope: hides the outer declaration of read
    string s = read(); // error: read is a bool variable, not a function
    // bad practice: usually it's a bad idea to declare functions at local scope
    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
}

 

Most readers will not be surprised that the call to read is in error. When the compiler processes the call to read, it finds the local definition of read. That name is a bool variable, and we cannot call a bool. Hence, the call is illegal.

 

Exactly the same process is used to resolve the calls to print. The declaration of print(int) in fooBar hides the earlier declarations of print. It is as if there is only one print function available: the one that takes a single int parameter.

 

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 a name is found, the compiler ignores uses of that name in any outer scope. Instead, the compiler assumes that the declaration it found is the one for the name we are using. What remains is to see if the use of the name is valid.

 

Image Note

In C++, name lookup happens before type checking.

 

 

The first call passes a string literal, but the only declaration for print that is in scope has a parameter that is an int. A string literal cannot be converted to an int, so this call is an error. The print(const string&) function, which would have matched this call, is hidden and is not considered.

 

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, because the compiler will see all three functions:

 

 

void print(const string &);
void print(double); // overloads the print function
void print(int);    // another overloaded instance
void fooBar2(int ival)
{
    print("Value: "); // calls print(const string &)
    print(ival);      // calls print(int)
    print(3.14);      // calls print(double)
}

 
Team LiB
Previous Section Next Section