Classic open-set polymorphism: virtual + override, owned via unique_ptr<Base>. std::function is the everyday type-erasure tool; CRTP covers static polymorphism.
struct Sink { virtual void write(Frame) = 0; virtual ~Sink() = default; };
std::vector<std::unique_ptr<Sink>> sinks;
std::function<void(Frame)> tap;
Generic lambdas make small hand-rolled type erasure and visitors less painful; no model change.
Closed-set polymorphism goes value-semantic: std::variant + std::visit (with the overloaded trick) replaces inheritance where the set of alternatives is known.
template <class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
using Event = std::variant<KeyDown, Resize, Quit>;
std::visit(overloaded{
[](const KeyDown& k) { handle(k); },
[](const Resize& r) { relayout(r); },
[](Quit) { stop(); }
}, ev);
Concepts become the compile-time interface: constrain templates instead of inheriting from interface classes when dispatch can be static.
template <class T>
concept Sink = requires(T t, Frame f) { t.write(f); };
void pump(Sink auto& out);
Deducing this is CRTP without the CRTP: mixins read the derived type from the call site. std::move_only_function erases move-only callables.
struct Clonable {
template <class Self>
auto clone(this const Self& self) { return Self(self); }
};
std::polymorphic<T> gives open-set polymorphism value semantics (deep copy through the base), and std::function_ref erases callables non-owningly at API boundaries.
struct Node { std::polymorphic<Expr> child; };
Node b = a;
C++29direction · not adopted
Direction: pattern matching (P2688) adds structural dispatch over variants and class hierarchies — a match for C++ that would absorb most visit call sites.