DEV Community

Lena
Lena

Posted on

The dangers of default value with virtual functions!

Article::Article

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

#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;
}
Enter fullscreen mode Exit fullscreen mode

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.

Sources

Top comments (2)

Collapse
 
pgradot profile image
Pierre Gradot

Les arguments par défaut, c'est pas ouf en vrai. J'ai écrit un long article sur ce sujet dev.to/younup/arguments-par-defaut...

Et d'ailleurs, récemment, clang-tidy m'a mis un warning sur une valeur par défaut pour une fonction virtuelle pure (le code était très vieux... et n'avait donc jamais fait ce qu'on avait pensé à la base)

Collapse
 
baduit profile image
Lena

Super ton article, surtout la partie "Sous le capot" !
Il y a des cas spécifiques où j'avoue que j'aime bien, genre l'exemple que tu fais avec le port série dans ton article, et avec std::source_location (bon ça c'est un cas très précis j'en conviens)

Il y a aussi SonarQube qui a un warning pour ça il me semble