Virtual Function Default Arguments Behaviour

virtual function default arguments behaviour

Default arguments are entirely compile-time feature. I.e. the substitution of default arguments in place of missing arguments is performed at compile time. For this reason, obviously, there's no way default argument selection for member functions can depend on the dynamic (i.e. run-time) type of the object. It always depends on static (i.e. compile-time) type of the object.

The call you wrote in your code sample is immediately interpreted by the compiler as bp->print(10) regardless of anything else.

Can virtual functions have default parameters?

Virtuals may have defaults. The defaults in the base class are not inherited by derived classes.

Which default is used -- ie, the base class' or a derived class' -- is determined by the static type used to make the call to the function. If you call through a base class object, pointer or reference, the default denoted in the base class is used. Conversely, if you call through a derived class object, pointer or reference the defaults denoted in the derived class are used. There is an example below the Standard quotation that demonstrates this.

Some compilers may do something different, but this is what the C++03 and C++11 Standards say:

8.3.6.10:


A virtual function call (10.3) uses
the default arguments in the
declaration of the virtual function
determined
by the static type of the pointer or reference denoting the object. An
overriding function in a derived
class does not acquire default arguments from the function it
overrides. Example:

struct A {
virtual void f(int a = 7);
};
struct B : public A {
void f(int a);
};
void m()
{
B* pb = new B;
A* pa = pb;
pa->f(); //OK, calls pa->B::f(7)
pb->f(); //error: wrong number of arguments for B::f()
}

Here is a sample program to demonstrate what defaults are picked up. I'm using structs here rather than classes simply for brevity -- class and struct are exactly the same in almost every way except default visibility.

#include <string>
#include <sstream>
#include <iostream>
#include <iomanip>

using std::stringstream;
using std::string;
using std::cout;
using std::endl;

struct Base { virtual string Speak(int n = 42); };
struct Der : public Base { string Speak(int n = 84); };

string Base::Speak(int n)
{
stringstream ss;
ss << "Base " << n;
return ss.str();
}

string Der::Speak(int n)
{
stringstream ss;
ss << "Der " << n;
return ss.str();
}

int main()
{
Base b1;
Der d1;

Base *pb1 = &b1, *pb2 = &d1;
Der *pd1 = &d1;
cout << pb1->Speak() << "\n" // Base 42
<< pb2->Speak() << "\n" // Der 42
<< pd1->Speak() << "\n" // Der 84
<< endl;
}

The output of this program (on MSVC10 and GCC 4.4) is:

Base 42
Der 42
Der 84

Virtual function default parameters and overloading

At some point in time future maintainers of your code will be baffled, confused, and/or perplexed if you change the default values depending on which static type they call foo on so I'm going to assume that's not your concern.

Given that your concern is someone changing the default in the parent and forgetting to update the child class that's easily solved with the non-virtual interface pattern:

#include <iostream>

using namespace std;

struct Base
{
void foo(int one = 1, int two = 2) { foo_impl(one, two); }

protected:
virtual void foo_impl(int one, int two)
{ cout << "one: " << one << " two: " << two << endl; }
};

struct Derived : public Base
{
protected:
virtual void foo_impl(int one, int two)
{ Base::foo_impl(one, two); cout << " derived!" << endl; }
};

int main()
{
Base* b = new Base();
Base* d = new Derived();

Derived* dp = new Derived();

b->foo();
d->foo();
dp->foo();

return 0;
}

Default Argument in Virtual Function

A *obj = new B();
obj->fun();

In this code, fun() is invoked polymorphically - the caller uses (only) knowledge of A::fun(), but the call is dispatched to a pointer that redirects to the implementation for B::fun(). That function argument - a / 5 - is provided by the caller before the redirected call though (way before - during compilation) - A's default is seen, not B's.

If you want something like you seem to expect, you might find it works to have A::fun(int a = -1) or some other sentinel value, with the implementations of fun checking for the sentinel value then replacing it with 5 or 10 as desired. That way, the implementation-specific values are incorporated during the call, not before.

Resolution of virtual function with default parameters

Your code is actually seen by the compiler like this:

(The display() method is not actually there, but the resolving works in similar way)

class A
{
public:
virtual void display(int i) { cout<< "Base::" << i << endl; }
void display() { display(5); }
};

class B : public A
{
public:
void display(int i) override { cout<< "Derived::" << i << endl; }
void display() { display(9); }
};

Now you should understand what happens. You are calling the non-virtual display() which calls the virtual function. In more strict words: the default argument is resolved just like if the no-arg non-virtual method was there - by the type of the variable (not by the actual type of the object), but the code gets executed according to real object type, because it is virtual method:

int main()
{
A * a = new B(); // type of a is A* real type is B
a->display(); // calls A::display() which calls B::display(5)

A* aa = new A(); // type of aa is A* real type is A
aa->display(); // calls A::display() which calls A::display(5)

B* bb = new B(); // type of bb is B* real type is B
bb->display(); // calls B::display() which calls B::display(9)
}

Virtual functions and default parameters

Compiling with clang -Wall the following warnings occur:

main.cpp:14:22: warning: 'B::f4' hides overloaded virtual function [-Woverloaded-virtual]
virtual void f4(int n){
^
main.cpp:6:22: note: hidden overloaded virtual function 'A::f4' declared here: different number of parameters (0 vs 1)
virtual void f4(){
^

These warnings explain what's going on. A virtual function is only overridden by another function with the same signature.

B::f4(int) does not override A::f4() because they have different parameter lists. Instead, it is a different virtual function.

So rac.f4() just calls A::f4() which is not overridden.

Since C++11 you can help to detect this problem:

virtual void f4(int n) override {
// ^^^^^^^^

Then the compiler will give an error if this function does not actually override something from the base.

Virtual functions default parameters

It's a bad idea because they aren't kept anywhere.

The default values that are used will be those defined in the static (compile-time) type. So if you were to change the default parameters in an override, but you called the function through a base class pointer or reference, the default values in the base would be used.

#include <iostream>

struct Base
{
virtual ~Base(){ }
virtual void foo(int a=0) { std::cout << "base: " << a << std::endl; }
};

struct Derived : public Base
{
virtual ~Derived() { }
virtual void foo(int a=1) { std::cout << "derived: " << a << std::endl; }
};

int main()
{
Base* derived = new Derived();
derived->foo(); // prints "derived: 0"
delete derived;
}

Good practice : Default arguments for pure virtual method

I often wish to use both default parameters and virtual function as you do. The others have rightfully pointed out however that this leads to ambiguity and is generally not a good idea. There is a reasonably simple solution, one that I use. Give your virtual function a different name, make it protected, and then provide a public function with default parameters which calls it.

class Base {
protected:
virtual void vSomeMethod(const SomeStruct& t ) = 0;
public:
void someMethod( const SomeStruc& t = 0 )
{ vSomeMethod( t ); }
}

Derived classes simply override vSomeMethod and don't worry at all about the default parameters.



Related Topics



Leave a reply



Submit