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:
- Never gonna Give you up
- Never gonna Let you down
- Never gonna Run around and desert you
- Never gonna Make you cry
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:
- Never gonna Run around and desert you
- Never gonna Make you cry
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.