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