Default arguments and overload resolution

Default arguments can be a surprisingly lengthy subject. What I want to cover here is more to do with how they work with declarations and inheritance. We’ll start by looking at overload resolution, then introduce default arguments, and finally virtual functions.

Overload resolution
It’s the process of selecting the best function to call based on the arguments of the function call (or the expressions which will result in the arguments) and the set of candidate function which could be called based on the call site [over.match / 13.3]. It’s a 2 step process:

1. The set of viable functions is determined based on the functions meeting the number of arguments and other criteria.
2. The best viable function is selected based on the implicit conversion rules to match each argument to the respective parameter of each viable function.

An interesting note is that accessibility is never considered. Meaning the best viable function for a call could be inaccessible, if say it was declared private. It would be considered ill-formed and your compiler should complain.

Enter default arguments

Functions with defaults arguments are also considered if the extra parameters have default arguments. This is where we leave overload resolution behind for a bit and look into a few default argument features [dcl.fct.default / 8.3.6]. Default arguments have to be specified in a function declaration. Interestingly, for non-template function declarations with default arguments, the default values can be added later in a later declaration in the same scope. For example:

void A(int, int);
void A(int, int = 42);

A(1);  // Calls A(1,42)

There are a few statements concerning scope and defaults:
1. Declarations in different scopes keep different sets of default arguments. Declarations in inner scopes don’t acquire default arguments from outer scopes and vice versa. The important keyword is “declarations”. If there is no new declaration in the inner scope, then the outer scopes are still valid and will be used.

2. If a parameter has a default argument in a declaration, then the subsequent parameters need to also have a default argument supplied either in this declaration or in a previous one. For example, if a function has 3 parameters, and the second parameter is set to a default value, then either the 3rd parameter is also defaulted in this declaration or in a previous declaration. Interestingly, this means you could have 2 declarations, the first with the 3rd parameter having it’s default set, and then subsequent declaration setting the 2nd parameter’s default argument. There’s an example below on this.

3. A default argument can’t be redefined in a later declaration.


void A(int, int);
void A(int, int = 42);

void B(){
   A(1);                  // Ok, there are no inner 
                          // declarations.

   void A(int = 1, int);  // Based on statement 1, 
                          // this inner declaration 
                          // can't use the defaults 
                          // from the outer scope.
}

void C(){
   void A(int, int);      // inner declaration.
   void A(int, int = 10); 
   void A(int = 12, int); // Based on statement 2, we can
                          // set a default argument for 
                          // parameter 1 since the previous
                          // declaration set a default
                          // argument for the 2nd parameter.

   void A(int = 13, int); // ERROR: can't redefine a 
                          // default argument as 
                          // statement 3 explains.
}

Enter dynamic polymorphism
The last item about default arguments to touch on is how it works with virtual functions. The default argument which is used in a virtual function call is based on the static type of the pointer or reference. This means default arguments in an overriding function are not inherited from the function it overrides.

struct Base {
virtual void f(int a = 42);
virtual void g(int b = 1);
};
struct Derived : public Base {
void f(int);
void g(int b = 0);
};

void m() {
Derived* pD = new Derived;
Base* pB = pD;
pB->f();    // Calls f(42)
pD->f();    // ERROR: the default argument not inherited.

pB->g();    // Calls g(1), as static type is Base. 
pD->g();    // NOTE: now calls g(0) as the static type
            // is of type Derived.
}

Final thoughts
As I hinted at the start, there’s quite a lot to cover with default arguments. I only wanted to focus here on the more interesting aspects, and only on functions (didn’t cover anything to do with templates). Perhaps I’ll expand on it in the future.

Leave a Reply

Your email address will not be published. Required fields are marked *