Code Yarns ‍👨‍💻
Tech BlogPersonal Blog

Do not use default arguments in C++ virtual functions

📅 2020-Jan-14 ⬩ ✍️ Ashwin Nanjappa ⬩ 📚 Archive

In an earlier post I described how default arguments are implemented in C++. Essentially, default arguments do not exist in the function definition generated by the compiler. They are added by the compiler at the call site, at compile time. They are not set at run time. This should already tell you why using default arguments in virtual functions can be harmful.

Conside the situation where you have a base class pointer pointing to a derived class object. If you call a virtual function from the base class pointer, it will be redirected to the right derived class virtual function thanks to the use of virtual table. However, the default arguments for this call are set by the compiler at compile time. Since the compiler knows only that the pointer is of base class at compile time, it sets the default arguments from the base virtual function for this class. This is typically not the intended behavior by the author of the code.

Here is an example:

#include <iostream>

struct A
{
    virtual void foobar(int x = 10) = 0;
};

struct B : A
{
    void foobar(int x = 99) final
    {
        std::cout << "B->foobar(" << x << ")\n";
    }
};

int main()
{
    B* b = new B;
    A* a = b;

    b->foobar();
    a->foobar();

    return 0;
}

The author probably intended this program to print:

B->foobar(99)
B->foobar(99)

However, it actually prints:

B->foobar(99)
B->foobar(10)

We can verify that the compiler sets the default arguments at the call site at compile time by checking the assembly code:

# tmp.cpp:21:     b->foobar();
        movq    -32(%rbp), %rax # b, tmp94
        movl    $99, %esi       #, <<<<<<<<<<<<<<< 99 as default argument
        movq    %rax, %rdi      # tmp94,
        call    _ZN1B6foobarEi  #
# tmp.cpp:22:     a->foobar();
        movq    -24(%rbp), %rax # a, tmp95
        movq    (%rax), %rax    # a_9->_vptr.A, _1
        movq    (%rax), %rax    # *_1, _2
        movq    -24(%rbp), %rdx # a, tmp96
        movl    $10, %esi       #, <<<<<<<<<<<<<<< 10 as default argument
        movq    %rdx, %rdi      # tmp96,
        call    *%rax   # _2