C++ Week 20

Function overload resolution

Resolving function overloads

In C++ you can have two or more functions with the same name so long as they differ in their parameter lists. This is called function overloading. The function is invoked whose parameter list matches the arguments in the call. Normally the compiler can deal with overloaded functions fairly easily by comparing the arguments with the parameter lists of the candidate functions. However, this is not always a straightforward matter. Consider the following code fragment:

void f(double d1, int i1)
{...
}
void f(double d1, double d2)
{...
}
...
int main( )
{  cout << f(1.0, 2);
}

How does the compiler know which version of f() to call? The compiler works through the following checklist and if it still can't reach a decision, it issues an error:

  1. Gather all the functions in the current scope that have the same name as the function called.
  2. Exclude those that don't have the right number of parameters to match the arguments in the call. (It has to be careful about parameters with default values; void f(int x, int y = 0) is a candidate for the call f(25);)
  3. If no function matches, the compiler reports an error.
  4. If there is more than one match, select the 'best match'.
  5. If there is no clear winner of the best matches, the compiler reports an error - ambiguous function call.

Best matching

In deciding on the best match, the compiler works on a rating system for the way the types passed in the call and the competing parameter lists match up. In decreasing order of goodness of match:

  1. An exact match, e.g. argument is a double and parameter is a double
  2. A promotion
  3. A standard type conversion
  4. A constructor or user-defined type conversion

Exact matches

An exact match is where the parameter and argument datatypes match exactly. Note that, for the purposes of overload resolution a pointer to an array of type x exactly matches a pointer of type x. This is because arrays are always passed by reference, meaning that you actually pass a pointer to the first element of the array. For example:

void f(int y[ ]);    // call this f1
void f(int* z);      // call this f2
....
int x[ ] = {1, 2, 3, 4};
f(x);         // Both f1 and f2 are exact matches, so the call is ambiguous.

void sf(const char s[]);
void sf(const char*);
....
sf("abc");   // Same problem; both sf functions are exact matches.

Type promotion

The following are described as "promotions":

Standard conversions

All the following are described as "standard conversions":

All of the standard conversions are treated as equivalent for scoring purposes. A seemingly minor standard conversion, such as int to long, does not count as any "better" than a more drastic one such as double to bool.

Constructors and user-defined conversions

A certain kind of constructor can play a special role in type conversion. Suppose you had a class Bigint that was capable of storing integral numbers larger than INT_MAX, and you had a constructor for a Bigint that took a C-string, so that a declaration of a Bigint object might look like this:

Bigint	b("12345678901234567890");

This constructor also provides an implicit type conversion. Having defined b as a Bigint, it would be possible to say, for example:

b = "999999999999";
or you could invoke the type-conversion explicitly:
b = static_cast<Bigint>("88888888888");

The same type-conversion would be used in parameter passing, enabling us to do this:

void f(Bigint);

f("77777777777777");
We are able to pass a C-string to a function that expects a Bigint because there exists a Bigint constructor that takes a C-string.

This kind of conversion can only work when the constructor can be called with just one argument. Generally this means that the constructor will have just one parameter, but it could have more if all but the first of the parameters (or, indeed, all of them) had default values.

When you think about using this type-conversion in assignment, it is obvious that the constructor must be of the kind that can be called with only one argument. In the statement:

b = something;
the "something" can't be nothing, and it can't be more than one thing.

In the context of parameter passing, the compiler has to be able to find a parameter for each argument. It would not consider void f(Bigint) as a candidate for f("6666666", 57); even if the Bigint class had a constructor that took a C-string and an int - the compiler needs one parameter for the "6666666" and another for the 57.

The compiler also has to find an argument for each parameter that is not given an explicit default in the function header, so it would not consider void f(Bigint) as a candidate for f(); even if Bigint had a constructor with no parameters or a constructor with all its parameters defaulted. (It would, however, consider void f(Bigint b = "0") as a candidate for f(); since the parameter of the function f (as opposed to the parameter of the Bigint constructor) has an explicit default.)

These "conversion constructors" enable us to have object parameters corresponding to arguments of other types.

User-defined conversions are for going the other way. They enable us to pass objects (as arguments) to functions with parameters of other types, as in the following:

void f(int n);

Bigint b("123456");
f(b);                // would work if there was a Bigint::operator int() conversion function

Conversion member-functions allow you to specify how you want objects to respond if they are asked to behave as if they were objects of some other type. In this case, a Bigint is being treated as if it were an int. A conversion function that allowed the above code to work might take the form:

class Bigint
{ public:
  ...
  operator int();    // returns a Bigint as an int.
  ...                // Note there is no return type and no parameter list.
};

Bigint::operator int()
{  If the value of the Bigint is less than INT_MAX,
        return the value as an int,
   else return -1;   // or throw an exception or something
}

explicit

It can happen that you have a constructor that can be called with just one argument, and which will therefore behave as a conversion constructor, but you don't want it to behave in this way. Perhaps we have a Bigint constructor that takes a string. This would mean that you could write something like:

Bigint b;
b = "987654321";
But implicit type-conversions are a common source of programming error, so you might decide to disallow that sort of conversion. We can do that simply by inserting the keyword explicit before the constructor prototype in the class definition:
class Bigint
{ public:
	......
	explicit Bigint(string);
	......
};
With that explicit before the constructor, we cannot now make use of implicit type-conversion from C-string to Bigint:
void proc(Bigint);
Bigint b;
b = "99999999999";  // Error! Implicit type conversion not allowed
proc("9999999999999");  // not allowed
b = static_cast<Bigint>("999999999999");  // OK, type conversion explicit
proc(Bigint("999999999999"));  // OK

A note on const ref parameters

Suppose that the Bigint class had a constructor that took an int. It would then be possible to pass an int to a function that expected a Bigint:

void proc(Bigint bx)
{ ...... }

proc(54321);
The parameter here was a value parameter. Would it have made any difference if the parameter had been a const ref, i.e.
void proc(const Bigint& bx)
{ ...... }

proc(54321);

At first sight, this looks rather strange. When you pass an argument by reference, the parameter name becomes an alternative to the argument name - two names for the same thing. But here, we are not passing anything that has a name; we are simply passing a value. To put it more technically, the argument has an r-value but not an l-value (you could use it on the right-hand side of an assignment but not on the left-hand side). There is, apparently, nothing for bx to refer to.

But in fact it would work. When you pass an argument that has an l-value to a const ref parameter, you get the ordinary call-by-reference (except, obviously, that the parameter is const). When you pass an argument that has only an r-value, a temporary variable is created with the same type as the parameter, and the const ref parameter refers to this temporary variable. So, in this example, a temporary variable of type Bigint is created, the value 54321 is used to initialise it, and this is the object to which bx refers. The temporary variable disappears when proc terminates. (In other words, when the argument has no l-value, the const ref parameter behaves very like a value parameter.)

Parameter lists that include default values

Parameters with default values carry their full weight in the scoring of a function. For example:

void f (int x, double y, int z = 2);   // (f1)
void f (int x, int y);                 // (f2)
...
f(3, 4.5); // matches f1 exactly, whereas f2 requires a double-to-int standard conversion

Choosing a winner

A candidate function is only as strong as its weakest match; a candidate requiring three promotions, for example, beats a candidate with two exact matches and a standard conversion. Candidates whose weakest matches are equivalently weak are compared on their next-weakest, and so on - a candidate with a standard conversion, a promotion and an exact match beats a candidate with a standard conversion and two promotions.


Notes on R. Mitton's lectures by S.P. Connolly, edited by R. Mitton, 2000