C++ Inheritance vs Composition Before Building an ECS¶
Movement, visibility, and lifetime can vary independently. This chapter implements the same simulation with inheritance and composition, then checks both against the plain baseline instead of assuming one architecture is better.
C++ Inheritance vs Composition: Keep the Problem Fixed¶
Use the previous chapter's simulation contract, including movement-before-expiration and ordered snapshots. All three implementations live in simulation.hpp, and tests.cpp runs the same contract against each.
The experiment changes representation, not the required behaviour. It does not test runtime editing of capabilities or interactions between bodies. Those would require more tests and potentially different design decisions.
Inheritance: An Interface for Behaviour¶
The inheritance variant owns bodies through std::unique_ptr<Body>.
The base has a virtual destructor, an advance operation, and an observation
method. This is an ordinary polymorphic interface; clients do not downcast.
The two behaviour axes produce four explicit specialisations:
The factory chooses a specialisation from the input flags. Visibility remains a data flag: it does not need another subclass axis. You do not need to write a template framework to understand the example; the two booleans simply select which operations the class implements.
This avoids inventing a deep hierarchy of projectiles and decorations. It also demonstrates a cost: a new independent behaviour can require edits to both the specialisation logic and factory combinations. Runtime capability changes would need a policy, such as replacing a body or delegating its behaviour to owned values.
This is one inheritance design, not evidence that every OOP program has combination explosion. A Strategy implementation can express different choices.
Composition: Optional Owned Values¶
The composition variant stores:
The body owns its optional values directly. Presence of motion means there
is movement data; absence means no movement behaviour. A missing lifetime
means the body does not expire.
The update loop checks these values:
This removes a separate “moving but has no movement data” flag/value pairing. It does not eliminate every invalid state: external inputs, numerical limits, and future cross-body references still need policies.
Adding optional data is not free. The record still contains optional storage, the loop still checks presence, and dependencies between behaviours still need to be expressed. None of that implies a measured speed advantage.
Why This Is Not Yet an ECS¶
We have reusable behaviour data, but it remains embedded in each body. There is no entity-ID registry, separate component store, or query that joins components by entity. The rendering snapshot is also not an entity handle.
For a concrete library model, the EnTT ECS documentation describes a registry and views over component combinations. Our small owned-value example deliberately stops before that machinery.
Likewise, the composition example is not a structure-of-arrays layout. Changing ownership style and changing physical storage layout are separate experiments.
Verify Equivalence, Not Superiority¶
Build and test using the Chapter 2 commands. The suite:
- Runs the known movement and expiration expectations against each world.
- Rejects invalid
dtwithout modifying each world. - Creates all eight moving/visible/expiring combinations.
- Compares state and rendering snapshots over several steps.
- Checks expected survivor counts and a trajectory independently of the baseline.
Passing means the implementations agree for these tested cases. It is not a proof of all possible inputs, a concurrency test, or a benchmark.
Choose by the Next Requirement¶
| Requirement | Candidate to investigate | Cost to examine |
|---|---|---|
| Small fixed workload | Keep the plain record | Irrelevant fields and flag maintenance |
| Interchangeable behaviour behind a stable interface | Inheritance or Strategy | Ownership, dispatch, factory maintenance |
| Independent per-body capabilities | Owned-value composition | Presence checks and dependency rules |
| Large batches selected by capability, stable identity | Consider separate stores and ECS | Lookup, structural changes, ID lifecycle |
These are investigation prompts, not automatic rules. Do not rewrite a working system simply to match the last row.
Exercise: A Fourth Independent Capability¶
Sketch “acceleration” in each representation without implementing it first. Identify the files and functions that would change. Specify whether acceleration is applied before or after movement, and add a numerical expectation.
Then ask whether you need a new architecture or just one field and one pass. The answer should follow your workload and invariants.
Previous: Plain structs and loops. Next: Entity identity and IDs. Return to the course overview and planned next stages.