102 / 163 · C++11 · 10 min
During construction and destruction: verify virtual calls, do not guess vtable layout
Implementation diagrams of the object model help in understanding cost, but they are not a cross-compiler ABI promise. This lesson starts from the construction semantics already read in Chapter 5, records behavior across the object lifetime via indirect virtual calls, and distinguishes language guarantees, historical implementation models, and as-yet-uninitialized derived members.
In this lesson
Inside the C++ Object Model
The entire body text, code, footnotes, and tables of all 7 chapters of the public 182-page reflowed edition have been read paragraph by paragraph: Chapter 1 PDF pp. 12–31, Chapter 2 pp. 31–54, Chapter 3 pp. 54–82, Chapter 4 pp. 82–111, Chapter 5 pp. 111–137, Chapter 6 pp. 137–160, Chapter 7 pp. 160–182; all 17 numbered figures (subfigures counted separately) have been viewed directly, including the last Figure 4.3 / PDF p. 100. “full” refers only to all substantial chapters of this file; it does not claim page-by-page collation of the 304-page print edition or that the print index not included in this file has been read. PDF page numbers are not print page numbers.
Edition, actual reading range, and original sources →Ask who can be called, not where the vptr is
Chapter 5 explains virtual calls during construction by the order in which the vptr is set; this is a model for understanding common implementations, not a requirement that programs modify the vtable. The standard does not specify that an object must contain a vtable pointer at some fixed offset. What we can actually verify portably is which override a virtual call on the current object selects during construction or destruction.
During the base-class construction phase the derived part is not yet ready, so a derived override cannot be called to read its members. When the derived constructor body begins executing, bases and members have already been initialized, and a virtual call inside the derived class can select the derived override. The boundary here is determined by the construction phase, not by which type was ultimately written in the outermost declaration.
Indirect calls obey the same rule
The example calls the non-virtual member sample from the Base constructor, and sample in turn calls the virtual function kind. This layer of indirection does not let the call jump to the not-yet-constructed Derived; the recorded result is still 1. The Derived constructor body records 2, and a call through a Base reference on the complete object also yields 2.
Destruction proceeds in the opposite direction: the Derived destructor body records 2, then the Base destructor body records 1. The log uses an external fixed-size array; it does not grow a container or allocate memory during destruction; the tracer’s lifetime covers the observed object. The output shows the observable call order and contains no addresses, object sizes, or compiler-specific details.
Do not let the observation mechanism change the safety premises
Here every virtual function that is called has a normal definition and does not read uninitialized data. Do not call a pure virtual function from a base-class constructor merely to exhibit a boundary, and do not let observation pointers outlive the log. Base provides a virtual destructor so that the destruction contract as a polymorphic interface is clear, even though this example actually uses automatic-storage-duration objects.
C++20 still follows these phase rules. Calling a virtual function from a member-initializer expression is not categorically forbidden, but bases must already have been initialized and the function must not read members that are not yet initialized; this example observes only inside constructor bodies so as not to mix in another set of issues. The book’s hidden parameters and vptr assignments are explanatory pseudocode; no fixed layout can be inferred from this output.
Pitfalls
- A virtual function called indirectly from a constructor via a non-virtual helper member is still constrained by the current construction phase; it will not therefore call a more-derived override.
- Do not treat one compiler’s vptr location as a standard requirement, and do not prove language semantics by reading or writing the vtable through reinterpret_cast.
Run an example
Minimum C++11 · complete program · Download .cpp
#include <array>
#include <cassert>
#include <cstddef>
#include <iostream>
struct Trace {
std::array<int, 5> values{};
std::size_t used = 0;
void add(int value) noexcept {
assert(used < values.size());
values[used++] = value;
}
};
class Base {
protected:
Trace& trace;
void sample() { trace.add(kind()); }
public:
explicit Base(Trace& t) : trace(t) { sample(); }
virtual ~Base() { sample(); }
virtual int kind() const noexcept { return 1; }
};
class Derived final : public Base {
public:
explicit Derived(Trace& t) : Base(t) { sample(); }
~Derived() override { sample(); }
int kind() const noexcept override { return 2; }
};
int main() {
Trace trace;
{
Derived object(trace);
const Base& view = object;
trace.add(view.kind());
}
const std::array<int, 5> expected{{1, 2, 2, 2, 1}};
assert(trace.used == expected.size());
assert(trace.values == expected);
for (std::size_t i = 0; i < trace.used; ++i) {
if (i != 0) std::cout << ' ';
std::cout << trace.values[i];
}
std::cout << '\n';
}
Compile locally
g++ -std=c++11 -Wall -Wextra -Wpedantic -pthread books-inside-cpp-object-model.cpp -o example && ./exampleExpected result
1 2 2 2 1
CHECK YOUR UNDERSTANDING
Close the answer. Explain it.
Change view.kind() in the ordinary-use phase to object.Base::kind(), leaving everything else unchanged. What is the log?
Show a reference answer
The log becomes 1 2 1 2 1. Explicitly qualifying Base::kind() suppresses virtual dispatch for this one call and selects the base-class implementation directly; it does not change the object’s dynamic type and does not affect the call rules of the preceding construction or following destruction. Changing the middle element of expected to 1 expresses the new experiment’s expectation.
Check the sources
- Inside the C++ Object Model §5.2, reflow PDF pp.124–127
- C++ working draft: construction and destruction [class.cdtor]
- C++ working draft: virtual functions [class.virtual]
Drafts and official chapters change. The version mark is only the example’s minimum.