90 / 163 · C++11 · 12 min
Virtual calls during construction: prepare configuration first, then enable the strategy
Virtual calls during construction do not enter a more-derived layer that is not yet ready. Observe the three dispatch phases via safe event logging, then distinguish language-allowed calls from unsound initialization design, and avoid treating override as a lifetime guarantee.
In this lesson
Effective C++: 55 Specific Ways to Improve Your Programs and Designs
Have read paragraph by paragraph the entire technical body of the third English edition, Items 1–55 (print pages 1–272) and Appendices A/B (273–279), plus reread paragraphs missed in conversion such as the end of Item 26; also checked figure by figure the scope, strategy, and inheritance diagrams on PDF pages 177–180, 197, 202, 214–215, and 219. full means all technical items and appendices; marketing pages and the index were not item-by-item verified as part of the technical read-through. This is not merely downloading, looking at the table of contents, or reading sample chapters.
Edition, actual reading range, and original sources →First locate which layer is currently being constructed
After reading third-edition Item 9, do not first interpret the title as a compiler prohibition. A base-class constructor calling a defined virtual function can be legal code; but the call uses the final overrider of the currently constructing layer and will not jump to a derived layer that has not yet been constructed. When the derived constructor body begins, the base class and that layer's members have already been initialized, so dispatch can use that layer's implementation.
The logger below records numbers during base construction, derived construction, and full-object use respectively. It does not read uninitialized members and does not call pure virtual functions, so it can observe the rule safely. The three results differ because the object is in different phases, not because the compiler ignored virtual.
Information needed for construction should be obtained from data that already exists
Suppose the base class needs to choose a cache size, but the selection function is overridden by the derived class and depends on derived members. Even if the call is hidden inside an ordinary member function, the initialization order does not change, and an indirect virtual call is still bound by the same rule. A sound design is to compute the size from constructor arguments first, then pass it to the base-class constructor. That computation can be a static function, explicitly independent of a half-built object.
Another need is to start work only after the object is complete; then external code can explicitly call an ordinary operation after construction succeeds. Do not register an unfinished object with a global callback system just to save one call; that lets other code access it before invariants hold.
Keep the observation experiment separate from production advice
The example deliberately retains a legal virtual call during construction to demonstrate the dispatch boundary; production interfaces should usually avoid relying on this phase difference for business logic. Destruction is the opposite direction: after entering the base-class destructor phase, you also cannot expect to call a derived override. If layered cleanup is truly needed, each layer should handle only the state it is responsible for.
This book was written before C++11; this example uses override to check the override relationship and noexcept to mark a non-throwing query. They do not change the lifetime rules. Also avoid over-generalizing: during current-class member initialization, after base initialization is complete, current-layer dispatch may legally occur; it cannot be simplified to “only the constructor body allows any virtual call.”
Pitfalls
- Calling a current-layer pure virtual function via virtual dispatch during construction or destruction yields undefined behavior; this example deliberately uses a defined ordinary virtual function.
- The recording container must outlive the observed object; this example constructs the recorder first, then the object, and destruction does not append to the recorder.
Run an example
Minimum C++11 · complete program · Download .cpp
#include <cassert>
#include <vector>
class Layer {
public:
explicit Layer(std::vector<int>& events) { events.push_back(level()); }
virtual ~Layer() = default;
virtual int level() const noexcept { return 10; }
};
class Ready final : public Layer {
public:
explicit Ready(std::vector<int>& events) : Layer(events) {
events.push_back(level());
}
int level() const noexcept override { return 20; }
};
int main() {
std::vector<int> events;
Ready object(events);
const Layer& view = object;
events.push_back(view.level());
assert((events == std::vector<int>{10, 20, 20}));
}
Compile locally
g++ -std=c++11 -Wall -Wextra -Wpedantic -pthread books-effective-cpp.cpp -o example && ./exampleExpected result
Expected: exit 0, no output; every assert holds.
CHECK YOUR UNDERSTANDING
Close the answer. Explain it.
Base-class initialization must obtain a capacity determined by an external parameter seed. How do you avoid depending on a derived virtual function?
Show a reference answer
Add an explicit capacity parameter to the base class; the derived constructor passes the result of the static function choose_capacity(seed) in the base initializer. choose_capacity only accesses seed and static constants, not this. Thus the base class obtains real input and does not need to call an override on a half-built object.
Check the sources
- Effective C++ 3rd, Item 9, pp.48–52
- C++ draft [class.cdtor] — construction, destruction and virtual calls
Drafts and official chapters change. The version mark is only the example’s minimum.