Baduit

A young French developper who really likes (modern) C++

About me
11 September 2022

The dangers of default value with virtual functions!

by Baduit

Article::Article

Here’s a snippet of code a friend send me:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <iostream>
 
class Base
{
    public:
        virtual void rick(int x = 0)
        {
            if (0 == x)
                std::cout << "Give you up\n";
            else
                std::cout << "Let you down\n";
        }
};
 
class Derived : public Base
{
    public:
        virtual void rick(int x = 10)
        {
            if (0 == x)
                std::cout << "Run around and desert you\n";
            else
                std::cout << "Make you cry\n";
        }
};
 
int main()
{
    Derived d1;
    Base* bp = &d1;
 
    std::cout << "Never gonna ";
    bp->rick();
 
    return 0;
}

Compiler explorer link Can you deduce what you will be printed? You can see the solution in the compiler explorer link just above.

Personally I did find the right solution but two things helped me: first, I knew my friend send me this snippet to try to trick me, secondly, the snippet is short and I can directly smell that there is something fishy with the virtual functions, the inheritance and the default value. In a real project, it may not be that simple to spot this.

Now let’s why it does act like this!

Explanations

A little recap

We have a base class named Base and a class inheriting from Base named Derived. (How original)
There is a virtual member function virtual void Base::rick(int x = 0) in Base and a function with the same name in Derived : virtual void Derived::rick(int = 10)
Both member functions print a different text depending on x value.

In the main functions, we have a Derived object that we access through a Base* to call the rick member function.

It means that if we try to guess what is the output there is four possibilities:

Step by step analysis

Let’s begin by deducing which rick will be called, rick is defined in Base and overridden in Derived, and yes it works that way even if the default value of x is different because the default value is not part of the prototype.
Now that we know that it is pretty easy to know which one is called: we have a Derived object, the function is virtual, we call it through a pointer, it will call virtual void Derived::rick
We have two possibility left:

You may tempted to answer “Never gonna Make you cry” because the default argument is 10 in virtual void Derived::rick(int x = 10) but that’s wrong, the real output is “Never gonna Run around and desert you” and the only bug here is between the chair and the keyboard :)

Why does it do that?

Because the standard says so:

The overriders of virtual functions do not acquire the default arguments from the base class declarations, and when the virtual function call is made, the default arguments are decided based on the static type of the object

It literally means that if you have an object of type Derived and you call rick() the default value of the argument does not depend ON which implementation is called.

Article::~Article

As we can see, the behavior is logic, but not intuitive and can be hard to spot. That’s why I think this should not be used, but if you are crazy enough to use it, there should be some comments and documentation to warn other devs about this.

Sources

tags: Cpp