Skip to content

C++ Records vs Composition vs Inheritance: One Change

All three representations implement the same acceleration request. Compare where a reader finds behaviour and where a maintainer edits—not only whether outputs match.

Read Three Implementations

In model.hpp:

  • RecordWorld owns Seed records and calls a direct function.
  • composition::World owns Body values with optional Motion and lifetime.
  • inheritance::World owns polymorphic Body objects, choosing UniformBody or AcceleratedBody.

The inheritance example uses two motion variants. Visibility and lifetime remain shared data; we do not manufacture a subclass for every combination to make inheritance look bad.

Composition represents the dependency structurally: an acceleration is owned inside Motion, so a Body without Motion has nowhere to put it.

1
2
3
4
5
6
7
struct Motion { Vec2 velocity; std::optional<Vec2> acceleration; };
struct Body {
    Vec2 position;
    bool visible;
    std::optional<Motion> motion;
    std::optional<double> lifetime;
};

The input adapter still validates Seed before constructing Body. The two concrete inherited motion constructors also reject incompatible input, including direct construction outside World.

Compare Actual Edit Points

Design Data and construction Dispatch Reader's cost
Records Optional field in Seed Branch in advance Flags and validation
Composition Acceleration in owned Motion Optional Motion check Nested values
Inheritance Constructor chooses motion variant Virtual update_motion Base contract and override selection

Shared numerical helpers preserve equations, not identical ownership or dispatch.

The inherited Body::advance retains lifetime ordering after virtual motion. Adding a subclass alone is insufficient if a new requirement changes that shared ordering.

Trace A in Each Version

For records, find fields and advance. For composition, follow Body → Motion → acceleration. For inheritance, inspect construction to learn the override, then return to the base for lifetime.

Composition keeps capability ownership explicit. Inheritance can clarify a small family of substitutable algorithms. Records minimise machinery. None is automatically best for every task.

Verify the Comparison

/tmp/pi-design-main/design_demo alternatives
/tmp/pi-design-main/design_tests alternatives

The demo ends with:

alternatives x: 2.5 2.5 2.5

Tests enumerate 12 valid combinations of movement, visibility, lifetime, and acceleration. Four stationary-acceleration combinations are excluded by the contract.

Full normalised snapshots are compared through several frames. Independently calculated x values provide another oracle. Later identity and ECS worlds participate in the same comparison.

Where Evidence Stops

Matching fixtures establishes tested behaviour, not maintainability or speed. Edit maps support judgement; they do not measure developer effort.

Composition here uses optional values, not a plugin registry. Inheritance offers two variants, not arbitrary runtime replacement. No hardware benchmark has been performed.

Exercise

Sketch “drag reduces velocity before movement.” Mark input, validation, construction, update, observation, and test edits in all three designs. Is drag an algorithm variant or an independently configurable capability? Define the requirement first.

Next: give persistent selection identity without ECS.


Previous: Chapter 3 · Course overview · Next: Chapter 5

Donate