Learning objectives
After this chapter you should be able to:
Do unaided:
- Draw a polymorphic object and its class's vtable, and trace a virtual call on the drawing.
- Predict which function runs for a call through a base pointer or reference, including non-virtual, qualified and in-constructor calls.
- Explain why a base deleted through a base pointer needs a virtual destructor, and read the ASan and Valgrind reports when it lacks one.
- Recognize slicing in a
std::vectorof base objects and in a by-value parameter, and write the declaration that makes it a compile error. - Choose inheritance or composition for a part of the station, and apply a written design-review checklist with a reason for each verdict.
May delegate:
- Implementing a class hierarchy from your own design (classes, relationships, ownership, tests), by the agent in the Pair role.
- A first-pass critique of that code against your checklist, which you then verify.
Opening case
A composite case, not a real student or course.
Sam's PA5 design fit on one page: an abstract stn::Sensor with four kinds below it, and a stn::Station that owns them through std::vector<std::unique_ptr<Sensor>> and steps simulated time a minute at a time. Sam wrote the class list, ownership rules and six tests, and handed the spec to the agent in the Pair role. It returned 300 lines. The base class began:
The chapter continues from here.
The loop in this chapter
Open a step to see what it asks of you and how it plays out in the Pair phase.
PredictBefore any code exists, predict each line of Listing 15.8, draw the vtables for your four sensor kinds, and count the virtual calls in a simulated day.
Step 1 of 6 · You, no AI
What you hand in: A written prediction: the output, the memory state, the operation count, and how sure you are.
Why this step: A prediction written before the run is the only way to learn whether your model of the machine is right. A miss is the most useful result there is, because it shows exactly where the model is wrong.
The first prediction is never delegated.
Ask yourself: What will this print, what will memory look like when it ends, and how many operations will it take?
Log the prediction in your work log · Predict in every chapter · ← Predict in Chapter 14 · Predict in Chapter 16 →
WriteBy hand, write
Sensorand one derived class, and check them against your drawing.Step 2 of 6 · You, no AI
What you hand in: One small piece written by hand, to build the mental model.
Why this step: You can only check what you could have built. Writing a small piece yourself gives you the model you will need to judge the agent's version later.
Ask yourself: Could I write this piece again tomorrow without notes?
Browse the companion code · Write in every chapter · ← Write in Chapter 14 · Write in Chapter 16 →
SpecifyWrite the class list with is-a and has-a labels and reasons, the ownership section, the tests (including station destruction under ASan) and the checklist in Table 15.1.
Step 3 of 6 · You
What you hand in: The header or class interface, unit tests, a complexity budget and the ownership rules.
Why this step: The specification is the contract the code will be held to. Tests written first, a cost budget and ownership rules turn "looks right" into something you can check.
The ownership and lifetime section of a specification is never delegated.
Ask yourself: Who allocates, who frees, what does it cost, and what input would break it?
Prompt and specification templates · Specify in every chapter · ← Specify in Chapter 14 · Specify in Chapter 16 →
DelegateThe Pair implements the other classes and reviews its code against your checklist; a separate Adversary session writes hostile client code.
Step 4 of 6 · The agent
What you hand in: The implementation from your specification, or an explanation in the Tutor phase, with the prompts and transcript saved.
Why this step: The agent does the typing; you keep the judgment. Saving the prompts and the transcript makes the delegation something you can audit afterward.
Pair Pair phase: the agent writes small units from your specification, and you audit every line.
Ask yourself: Did I hand over my specification and tests, and did I save what the agent said?
Prompt templates · Delegate in every chapter · ← Delegate in Chapter 14 · Delegate in Chapter 16 →
AuditBuild with
-Wnon-virtual-dtoradded; run ASan, UBSan and Valgrind; apply the checklist yourself; run the Adversary's cases; measure dispatch in proportion; decide keep or rewrite.Step 5 of 6 · You and the tools
What you hand in: The Audit Report: tests, sanitizer and Valgrind output, your own cost derivation against a timing plot, the defects found, and a keep-or-rewrite decision.
Why this step: What the agent says is testimony until an instrument confirms it. Tests, sanitizers, Valgrind and a timing plot are evidence.
The final judgment that the code is correct is never delegated.
Ask yourself: What did the sanitizer report, does the timing plot match my derivation, and would I keep this code or rewrite it?
The Audit Report template · Audit in every chapter · ← Audit in Chapter 14 · Audit in Chapter 16 →
DefendIn five minutes: why each inheritance is there, what happens to memory when the station is destroyed, and what dispatch costs in a simulated day.
Step 6 of 6 · You, live
What you hand in: A five-minute oral defense.
Why this step: If you can explain it live, you own it. The oral checks that the understanding is yours and not the agent's.
Ask yourself: Why this bound, what input breaks it, and where is this memory freed?
How the loop works · Defend in every chapter · ← Defend in Chapter 14 · Defend in Chapter 16 →
What agents get wrong here
Patterns observed in agent-written code, to test for, not guarantees. If the agent no longer makes the mistake, your job is to prove it got it right, with the same instrument.
A polymorphic base without a virtual destructor
How to catch it Read the base destructor first; -Wnon-virtual-dtor; ASan run that destroys a full station
Section 15.3.1 · Compiler warnings, AddressSanitizer
Slicing in containers, parameters and range-for variables
How to catch it Delete the base's copy operations; test derived behavior through the container
Section 15.3.2 · Tests
Inheritance for reuse where composition fits
How to catch it The design checklist (Table 15.1); say "every X is a Y" aloud
Section 15.3.3 · Your own trace
What the chapter covers
- 15.1 The concept, by hand
- 15.1.1 Inheritance and overriding
- 15.1.2 Dynamic dispatch and the vtable
- 15.1.3 Abstract classes: the PA5 hierarchy
- 15.1.4 Virtual destructors
- 15.1.5 Slicing
- 15.1.6 Composition and substitutability
- 15.2 Predict and check
- 15.3 What agents get wrong here
- 15.3.1 Missing virtual destructors
- 15.3.2 Slicing in containers and parameters
- 15.3.3 Inheritance where composition fits
- 15.4 Working with the agent
- 15.5 Lab: PA5, the simulated station
- 15.6 Defend it
Cost thread
The design cost people argue about most is dynamic dispatch. A virtual call loads the vptr, loads a slot and jumps through a pointer the processor must predict; it can also block inlining, the compiler's pasting of a small function's body into its caller. Here is g++ 13 at -O2 compiling return s.calib(x); for a const Sensor& s (from -S output, names demangled):
Summary
- A non-virtual call is chosen at compile time from the static type; a virtual call through a pointer or reference, at run time from the dynamic type.
overridemakes the compiler check that a function really overrides. - Compilers implement dispatch with one vtable per class and one vptr per object; the standard leaves the layout to the compiler, but gdb's
info vtblshows it. During a base constructor the vptr points at the base table. - Deleting a derived object through a base pointer without a virtual destructor is undefined behavior; on g++ 13, ASan reported 40 bytes allocated and 16 freed, and 11,520 bytes leaked. Inside
std::unique_ptr, g++ gave no warning; clang did. - Copying a derived object into a base object slices it, with no warning and no sanitizer report. Deleting the base's copy operations turns every slice into a compile error.
- Use public inheritance only for substitutable is-a relationships; prefer composition for reuse and run-time features. Calibration is a part, not a parent.
- A predictable virtual call cost about as much as a non-inlined call (about 2.5 ns here); random targets, about four times more. In a simulated day, dispatch was a few percent of the cost of formatting the log.
Key terms
- abstract class: a class with at least one pure virtual function; it cannot be instantiated.
- base subobject: the base-class object inside every derived object.
- composition: building a class from member objects of other classes (a has-a relationship).
- dynamic dispatch: choosing at run time, from the dynamic type, which function a call runs.
- dynamic type: the type of the object a pointer or reference actually refers to.
- final: a specifier forbidding further derivation from a class or further overriding of a virtual function.
- hiding: a derived member that shadows a same-named base member only for calls through the derived type.
- inheritance: declaring a class as an extension of a base class, which it contains.
- override: a specifier asserting that a function replaces a base virtual function, checked by the compiler.
- polymorphism: one call site running different code for objects of different types.
- pure virtual function: a virtual function declared
= 0, which a derived class must override to be concrete. - slicing: copying a derived object into a base-type object, keeping only the base part and its behavior.
- substitutability: a subtype keeping every promise of its supertype (Liskov substitution principle).
- virtual destructor: a destructor declared
virtual, so deleting through a base pointer runs the derived destructor. - virtual function: a member function whose calls through a base pointer or reference follow the dynamic type.
- vptr: the hidden pointer in each polymorphic object to its class's vtable (not a standard term).
- vtable: a per-class table of function pointers that compilers use to implement virtual calls.
Companion code
11 files for this chapter, compiled with the book's flags.
Read the code Download ch15.zip
Connections
Where this chapter and another point to each other most often.
- Chapter 14: Resource Ownership: RAII, the Rules of Zero, Three, and Five, and Smart Pointers
It assumes classes (Chapter 13) and destructors, the rule of five andstd::unique_ptr(Chapter 14). ### 15.1.1 Inheritance and overriding A temperature sensor is a sensor with a particular unit and a particular way of producing values.
Further reading
- Gamma, Helm, Johnson and Vlissides (1994), Chapter 1. The original case for favoring composition, stated more carefully than most summaries.
- Liskov and Wing (1994). The formal definition of behavioral subtyping; read the introduction and examples, and leave the proofs for later.
- Stroustrup and Sutter (2024), the class-hierarchy rules C.120 to C.139, plus C.35 and C.67. Short numbered rules, each with a reason and an example.
- Meyers (2014), Item 12. Every way an intended override can silently fail to override, and why the keyword fixes it.