CodeRigor

FREE LESSON

Core Guidelines: polymorphic interfaces without slicing

Choose value types or abstract interfaces deliberately; prevent slicing, preserve dynamic types through clone, and copy owned operations safely.

A class hierarchy is a design choice, not an upgrade over a concrete value. Start with an ordinary type that keeps its invariant and owns its resources. Choose an abstract interface when callers genuinely need an open family of implementations. This lesson extends the rule-of-zero/five and ownership lessons with C.35, C.67, C.121, C.128, C.130 and C.145.

1. Put lifetime policy in the base interface

If clients delete derived objects through a base pointer, the base needs a public virtual destructor. Another valid policy is a protected non-virtual destructor that prevents public deletion through the base. A public non-virtual destructor is unsafe for that polymorphic deletion contract. unique_ptr does not repair an unsuitable base destructor automatically.

The interface includes destruction
A base owner must dispatch destruction as well as normal operations. This interface supports deletion through Operation.
The interface includes destruction
A base owner must dispatch destruction as well as normal operations. This interface supports deletion through Operation.
C++
#include <memory>
#include <print>
struct Interface { virtual ~Interface() = default; virtual int value() const = 0; };
struct Concrete final : Interface { int value() const override { return 7; } };
int main() { std::unique_ptr<Interface> x = std::make_unique<Concrete>(); std::println("{}", x->value()); }

An abstract base can have no public data and no meaningful default value of its own. Keep derived state private where an invariant needs protection. A const virtual member promises the operation can run through a const base reference; a different signature in the derived class is not an override. The override keyword turns that mistake into a diagnostic.

2. Keep value copying out of a polymorphic base

Copying a derived object into a base value slices off the derived part. Passing the base by reference preserves dynamic dispatch without transferring ownership. A unique_ptr expresses ownership when the caller needs it. Suppress public base copy/move operations rather than letting slicing become an accidental API feature.

Copy the dynamic object, not a base slice
A virtual clone returns a new owner whose object preserves the concrete implementation and state.
Copy the dynamic object, not a base slice
A virtual clone returns a new owner whose object preserves the concrete implementation and state.

The build's clone returns unique_ptr. Smart-pointer specializations do not participate in raw-pointer covariant return rules: an override returning unique_ptr is not a matching override of unique_ptr. Return the base-owning type and construct the appropriate derived object inside it.

Prefer composition or a variant when the set of alternatives is fixed and value semantics fit. The existing variant/polymorphic lesson compares that decision. This task uses an open interface so tests can supply new operations.

3. Make deep copying a deliberate operation

A Pipeline owns a vector of unique_ptr. Its implicit copy is disabled by unique ownership, so a deep copy must call clone on every step. Do not guess the concrete type from one observed output, and do not share raw owning pointers between the original and copy. Changing an operation in the original must not mutate the copied pipeline's object.

Copy first, then commit
A temporary owns partially cloned steps. If cloning fails it cleans itself up, leaving the destination unchanged.
Copy first, then commit
A temporary owns partially cloned steps. If cloning fails it cleans itself up, leaving the destination unchanged.

Copy assignment can construct a complete replacement and swap after success. Self-assignment remains valid. Move and swap should be non-throwing when their members support that promise; adding noexcept to a throwing implementation does not implement recovery. Once you customize copying, audit the complete special-member policy so copying, moving and destruction remain consistent.

4. Avoid hidden virtual-interface traps

Calls from base construction/destruction do not dispatch into a not-yet-built or already-destroyed derived part. Keep virtual work out of those phases. Default arguments are selected from the static call interface, so differing defaults on overrides are misleading even when dispatch is virtual.

final is useful when a particular implementation is intentionally closed; it is not required for every concrete class. Multiple inheritance, virtual bases, cross-casts and ABI/Pimpl design deserve separate design review; this lesson does not pretend that the small Pipeline exercises all those decisions.

Apply it

Implement Operation, Shift and Pipeline. Tests provide unfamiliar derived operations, mutate original state, destroy through base owners and make clone throw. Identify which behavior demonstrates dispatch, independent ownership, and the assignment failure guarantee. Use the linked Guidelines for the reasoning behind the interface, rather than memorizing a single class recipe.

Watch & read

Free lectures, talks and readings that cover this skill from another angle. All resources →

Watch for the design rationale; use the authored lesson and current guideline links for the precise exercise contract.