Skip to content

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

Open in Compiler Explorer ↗ Quiz this entry