51 Intermediate The Hundred
The Virtual Call That Isn't
polymorphism keeps office hours
#include <iostream>
#include <string>
struct Instrument {
Instrument() { std::cout << "tuning a " << name() << "\n"; }
virtual ~Instrument() { std::cout << "packing up the " << name() << "\n"; }
virtual std::string name() const { return "generic instrument"; }
};
struct Cello : Instrument {
std::string name() const override { return "cello"; }
};
int main() { Cello c; }
Run it. What does it print?
Answer
tuning a generic instrument, then packing up the generic instrument — the cello never gets a say.
Why¶
While Instrument's constructor runs, the Cello part of the object does not exist yet —
its members are uninitialized and its overrides are not installed. C++ therefore rules that
during construction the object's dynamic type is the class whose constructor is executing,
so name() dispatches straight to Instrument::name(). That is deliberate protection, not
a missed optimization: a virtual call into the half-built Cello could touch members that
have not been created. Destruction plays the film backwards — by the time ~Instrument()
runs, the Cello part has already been destroyed, so the object is a plain Instrument
again. No warning saves you here: -Wall -Wextra compiles this in silence, because it is
perfectly legal C++. If name() were pure virtual the call would be undefined
behavior, and that one GCC does catch — pure virtual ... called from constructor, and
the program never even links.
The fix¶
Don't call virtuals from constructors or destructors — pass what the base needs up as a constructor argument:
struct Instrument {
explicit Instrument(const std::string& n) { std::cout << "tuning a " << n << "\n"; }
// ...
};
struct Cello : Instrument {
Cello() : Instrument("cello") {}
};
If the base genuinely must invoke derived behavior, use a factory that constructs first and
calls a virtual init() after — on a complete object, dispatch works normally.
Takeaway: inside a constructor or destructor the object is the class whose ctor/dtor is running — virtual calls there never reach a derived override.
Try it: g++ -std=c++17 main.cpp -o demo && ./demo