Virtual dispatch and vtables, destructors through base pointers, slicing, covariant returns, name hiding, multiple and virtual inheritance layout, the empty base optimization, and CRTP
Every question here is one question in disguise: what does the compiler know about an object at compile time (its static type), what does it learn at run time (its dynamic type), and how is the object laid out so the second can be found from the first? Sizes assume a 64-bit Itanium C++ ABI target (GCC or Clang; MSVC differs on some byte counts): pointers 8 bytes, int 4, double 8.
Each polymorphic class has a vtable, a static array of function pointers plus type information, and each object holds a hidden pointer to it, the vptr, at offset 0 of its subobject (one per polymorphic base). A virtual call p->f() compiles to: load the vptr from *p, load the slot for f, call it. That is the direct cost; the hidden one is lost inlining.
Constructors set the vptr: Base() installs Base's table, Derived() later overwrites it. So a virtual call inside a constructor dispatches to the class whose constructor is running, never further down: the derived part does not exist yet. Destruction reverses this: ~Base() resets the vptr before its body starts.
#include <cstdio>
struct Base {
virtual const char* name() const { return "Base"; }
virtual ~Base() { std::printf("~Base sees %s\n", name()); }
};
struct Derived : Base {
const char* name() const override { return "Derived"; }
~Derived() { std::printf("~Derived sees %s\n", name()); }
};
int main() { Derived d; std::printf("%zu\n", sizeof(Base)); }
Output: 8, ~Derived sees Derived, ~Base sees Base. The 8 is the vptr alone; by the same rule, name() called from Base() returns Base. A virtual call to a pure virtual function from there is undefined behaviour ([class.abstract]/6); an ordinary virtual call is not.
Check your understanding
The program above printed 8 for sizeof(Base). On the same 64-bit target, what does sizeof(Derived) evaluate to?
delete p with static type Base and dynamic type Derived is well defined only if Base has a virtual destructor; otherwise it is undefined behaviour ([expr.delete]/3), not merely a leak. std::unique_ptr<Base> changes nothing: its default deleter is delete on a Base*.
#include <cstdio>
#include <memory>
#include <vector>
struct Handler {
virtual void run() { std::puts("run"); }
~Handler() { std::puts("~Handler"); } // polymorphic, yet the destructor is not virtual
};
struct Buffered : Handler {
std::vector<int> buf = std::vector<int>(1000);
~Buffered() { std::puts("~Buffered"); }
};
int main() {
std::unique_ptr<Handler> h = std::make_unique<Buffered>();
h->run();
} // h's deleter runs delete on a Handler*: undefined behaviour
Output: run, ~Handler. ~Buffered never runs and the vector leaks. Because Handler is polymorphic, Clang warns (delete called on non-final 'Handler' that has virtual functions but non-virtual destructor; GCC: -Wdelete-non-virtual-dtor); for a base with no virtual functions both stay silent. The fix is virtual ~Handler() = default;.
Copying a Derived into a Base runs Base's copy (or move) constructor, whose parameter binds to the base subobject and copies only Base's members. The result has Base's vptr and none of Derived's state.
#include <cstdio>
#include <vector>
struct Shape { virtual double area() const { return 0; } virtual ~Shape() = default; };
struct Square : Shape {
double s; explicit Square(double s) : s(s) {}
double area() const override { return s * s; }
};
int main() {
std::vector<Shape> v;
v.push_back(Square(3)); // copies only the Shape subobject
std::printf("%g\n", v[0].area());
Square q(3); Shape& r = q;
std::printf("%g\n", r.area());
}
Output: 0, 9. A std::vector<Shape> holds Shapes and nothing else; polymorphism needs indirection, std::vector<std::unique_ptr<Shape>>. An abstract base turns the mistake into a compile error.
An override may return a pointer or reference to a class derived from the base's return type: Derived* clone() overrides Base* clone(), so d.clone() needs no cast. Smart pointers are not covariant.
Default arguments go the other way: they belong to the declaration, so they are filled in at compile time from the static type while the call dispatches on the dynamic type, and an override does not inherit them.
#include <cstdio>
struct Base {
virtual Base* clone() const { return new Base(*this); }
virtual void report(int depth = 10) const { std::printf("Base %d\n", depth); }
virtual ~Base() = default;
};
struct Derived : Base {
Derived* clone() const override { return new Derived(*this); } // covariant
void report(int depth) const override { std::printf("Derived %d\n", depth); }
};
int main() {
Derived d;
Derived* c = d.clone(); // a Derived*, no cast
Base* p = c;
p->report(); // default 10 comes from Base's declaration
// c->report(); // error: too few arguments, Derived::report has no default
delete c;
}
Output: Derived 10. p->report() has static type Base*, so 10 is substituted, then the vtable sends the call to Derived::report. c->report() fails (too few arguments to function call, expected 1, have 0). Had Derived declared depth = 99, the same body would receive 10 through p and 99 through c. Never redeclare a default on an override; put it on a non-virtual wrapper around a private virtual instead.
dynamic_cast reads the type information behind the vptr, so a downcast needs a polymorphic source. On a pointer a failed cast yields nullptr; a reference cannot be null, so it throws std::bad_cast. static_cast checks nothing: a downcast is defined only if the object really is a Dog (or has one as a base); otherwise the cast itself is undefined behaviour ([expr.static.cast]/11).
#include <cstdio>
#include <typeinfo>
struct Animal { virtual ~Animal() = default; };
struct Dog : Animal { void bark() { std::puts("woof"); } };
struct Cat : Animal { int lives = 9; };
int main() {
Cat cat; Dog dog; Animal& a = cat;
Dog* p = dynamic_cast<Dog*>(&a);
std::puts(p ? "dog" : "null");
try { Dog& d = dynamic_cast<Dog&>(a); d.bark(); }
catch (const std::bad_cast&) { std::puts("bad_cast"); }
Animal* pd = &dog;
static_cast<Dog*>(pd)->bark(); // fine: pd really points at a Dog
// static_cast<Dog*>(&a)->bark(); // UB at the cast itself: a is a Cat
}
Output: null, bad_cast, woof. The commented line compiles, and in practice the compiler adjusts the address (by zero here, nonzero under multiple inheritance) so bark would appear to work, but the language promises nothing from the cast onwards. static_cast down only when the dynamic type is known.
Name lookup stops at the first scope that declares the name; only then does overload resolution run. A derived class that declares any f hides every base f.
#include <cstdio>
struct Base {
void f(int) { std::puts("Base::f(int)"); }
void f(double) { std::puts("Base::f(double)"); }
};
struct Derived : Base {
using Base::f; // remove this line and d.f(2.5) fails
void f(const char*) { std::puts("Derived::f(const char*)"); }
};
int main() { Derived d; d.f("hi"); d.f(2.5); }
Output: Derived::f(const char*), Base::f(double). Without the using-declaration, d.f(2.5) is rejected (cannot initialize a parameter of type 'const char *' with an rvalue of type 'double'): the base overloads were never candidates. The error is the lucky case: had Derived declared f(int) instead, d.f(2.5) would compile and truncate to 2 (at most a conversion warning). A derived void g(int) next to a base virtual void g() likewise hides rather than overrides, so write override on every override.
With two bases only one can sit at offset 0. The other follows it, and converting C* to B* adds that offset; pb == &obj converts first and is true although the addresses differ as void*. A virtual call through the B* enters a small thunk that subtracts the offset before jumping to C's override, so this in the body is the complete object.
#include <cstdio>
struct A { int a = 1; virtual ~A() = default; };
struct B { int b = 2; virtual int sum() const { return b; } virtual ~B() = default; };
struct C : A, B { int c = 4; int sum() const override { return a + b + c; } };
int main() {
C obj; A* pa = &obj; B* pb = &obj;
std::printf("%td %td\n", (char*)pa - (char*)&obj, (char*)pb - (char*)&obj);
std::printf("%d %zu\n", pb->sum(), sizeof(C));
}
Output: 0 16, 7 32. A is the primary base at 0 (vptr, a at 8); B starts at 16 (vptr, b at 24); c lands at 28; total 32. The 7 shows the thunk working: through pb, 16 bytes in, C::sum still reads a at offset 8.
If L and R both derive from Top non-virtually, Bottom : L, R contains two Tops and n.t is ambiguous (member 't' found in multiple base-class subobjects). Declaring the inheritance virtual gives the most-derived class a single shared Top, placed after all non-virtual parts. Its offset depends on the most-derived type, so every class with a virtual base gets a vptr and reaching t from an L* costs an extra lookup.
#include <cstdio>
struct Top { int t; };
struct L : virtual Top { int l; };
struct R : virtual Top { int r; };
struct Bottom : L, R { int b; };
struct LN : Top { int l; }; struct RN : Top { int r; };
struct BottomN : LN, RN { int b; };
int main() {
Bottom d; d.t = 1; // one shared Top: unambiguous
std::printf("%zu %zu %zu\n", sizeof(L), sizeof(Bottom), sizeof(BottomN));
// BottomN n; n.t = 1; // error: ambiguous, two Top subobjects
}
Output: 16 40 20. L alone: vptr, l at 8, Top at 12. Bottom: L's non-virtual part at 0, R's at 16, b at 28, the shared Top at 32, padded to 40. BottomN: 8 + 8 + 4, with two copies of t. Only the most-derived constructor initializes the shared base; a Top initializer written in L or R is ignored when a Bottom is built.
An empty base (no data members, no virtual functions) may occupy zero bytes: the empty base optimization. An empty data member cannot, since every complete object needs a distinct address, so sizeof(Empty) is 1 and the member costs 1 byte plus padding (unless marked [[no_unique_address]], C++20). The curiously recurring template pattern rides on this: the base is a template on the derived class, so it can static_cast *this down to a type known at compile time; the call inlines and the base costs nothing.
#include <cstdio>
struct Empty {};
struct Member { Empty e; double x; };
struct Inherits : Empty { double x; };
template <class D> struct Priced {
double value() const { return static_cast<const D&>(*this).value_impl(); }
};
struct Bond : Priced<Bond> { double face; int n; double value_impl() const { return face + n; } };
struct PricedV { virtual double value() const = 0; virtual ~PricedV() = default; };
struct BondV : PricedV { double face; int n; double value() const override { return face + n; } };
int main() {
Bond b; b.face = 100; b.n = 3;
std::printf("%zu %zu\n", sizeof(Member), sizeof(Inherits));
std::printf("%g %zu %zu\n", b.value(), sizeof(Bond), sizeof(BondV));
}
Output: 16 8, 103 16 24. Member spends 1 byte on e, pads to 8, then x; Inherits puts x at offset 0. Bond is a double and an int padded to 16; BondV adds a vptr in front and pads to 24. What CRTP gives up is a common runtime type: Priced<Bond> and Priced<Swap> are unrelated, so no std::vector<Priced*>.
Common bug. Trusting
sizeof(Derived) == sizeof(Base) + sizeof(members): tail padding reuse (a derived member may sit in a non-POD base's trailing padding), the empty base optimization and shared virtual bases all break the sum.
Interview Tip. Default arguments, overload resolution and name lookup are static; which body a virtual call runs is dynamic, except inside constructors and destructors.
One linked problem per rule: Virtual Dispatch During Construction; Deleting a Derived Object Through a Base Pointer; Object Slicing in a Container of Base Objects; Covariant Return Types in a clone() Hierarchy; Default Arguments Meet Virtual Dispatch; dynamic_cast Failure Modes and a static_cast Trap; One Overload Hides the Whole Base Set; Pointer Adjustment Across Multiple Base Subobjects; Diamond Inheritance and Virtual Base Subobjects; Empty Base Optimization and Object Size; The Price of an Interface: sizeof Edition.