Part IV · C++ and Objects

15 Inheritance, Polymorphism, and Composition: Reviewing an Agent's Design

PairWeek 12Lab: PA5, the simulated stationOwnershipInterfaces

What still holds

Design choices have costs you must be able to name.

The agent may: implement your hierarchy from your design, then critique it against your checklist.

Learning objectives

After this chapter you should be able to:

Do unaided:

  1. Draw a polymorphic object and its class's vtable, and trace a virtual call on the drawing.
  2. Predict which function runs for a call through a base pointer or reference, including non-virtual, qualified and in-constructor calls.
  3. Explain why a base deleted through a base pointer needs a virtual destructor, and read the ASan and Valgrind reports when it lacks one.
  4. Recognize slicing in a std::vector of base objects and in a by-value parameter, and write the declaration that makes it a compile error.
  5. 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:

  1. Implementing a class hierarchy from your own design (classes, relationships, ownership, tests), by the agent in the Pair role.
  2. 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.

  1. 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?

  2. WriteBy hand, write Sensor and 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?

  3. 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?

  4. 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?

  5. AuditBuild with -Wnon-virtual-dtor added; 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?

  6. 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?

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

  1. 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
  2. 15.2 Predict and check
  3. 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
  4. 15.4 Working with the agent
  5. 15.5 Lab: PA5, the simulated station
  6. 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. override makes 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 vtbl shows 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.

See it on the map

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.