Skip to content

52 Intermediate The Hundred

Derived Body, Base Default

the override you wrote, with an argument you didn't

#include <iostream>
#include <string>

struct Base {
    virtual ~Base() = default;
    virtual void greet(std::string who = "base") {
        std::cout << "Base::greet, hello " << who << "\n";
    }
};

struct Derived : Base {
    void greet(std::string who = "derived") override {
        std::cout << "Derived::greet, hello " << who << "\n";
    }
};

int main() {
    Base* p = new Derived;
    p->greet();
    delete p;
}

Run it. What does it print?

Answer

Derived::greet, hello base — Derived's body runs, with Base's default argument.

Why

Virtual dispatch is dynamic; default arguments are static. A default argument is not part of the function that runs — it is pasted in at the call site, from the declaration the compiler finds through the static type of the expression, here Base*. So p->greet() becomes p->greet("base") at compile time, and only at run time does the vtable route that call into Derived::greet. One function, two personalities: call the same object through a Derived* and the very same line prints hello derived. GCC raises no warning for the mismatched defaults, even with -Wall -Wextra -Wpedantic — though clang-tidy's google-default-arguments check flags both declarations.

The fix

Never repeat — let alone change — a default argument on an override. Either the default lives in the base only, or, cleaner, keep defaults away from virtuals entirely with a non-virtual wrapper (the non-virtual interface pattern):

struct Base {
    void greet(std::string who = "base") { do_greet(who); }  // the one and only default
private:
    virtual void do_greet(std::string who);                  // overrides go here
};

Takeaway: the body is chosen from the dynamic type, the default argument from the static type — give a virtual function's default at most once, in the base.

Try it: g++ -std=c++17 main.cpp -o demo && ./demo

Open in Compiler Explorer ↗ Quiz this entry