Policy-Based Design
- Description: Compose class behavior by passing orthogonal policies as template parameters that the host class inherits from — compile-time Strategy with zero runtime cost; popularized by Alexandrescu's Modern C++ Design.
- My Notion Note ID: K2C-2-25
- Created: 2026-05-22
- Updated: 2026-05-22
- License: Reuse is very welcome. Please credit Yu Zhang and link back to the original on yuzhang.io
Table of Contents
- 1. Core Idea
- 2. Structure
- 3. C++ Example
- 4. C++20 with Concepts
- 5. When to Use / When Not To
- 6. Pitfalls
- 7. Related Patterns
- 8. References
1. Core Idea
- A class's behaviour decomposes along orthogonal axes (storage, threading, checking, allocation, error reporting, ...).
- Each axis = one policy = a template parameter that supplies a small interface (typedefs + methods).
- Host class takes one template param per axis, inherits from each policy → policies' members become host members.
- Pick policies at instantiation time:
Stack<int, FixedSize<128>, NoThreading, AssertChecked>. - Alexandrescu's Modern C++ Design (2001) → Loki library; popularized the technique.
- Same intent as Strategy, but resolved at compile time → no virtual calls, full inlining, but the strategy is baked in per instantiation.
2. Structure
- Host class (often called the host) — template with one type parameter per policy axis. Inherits publicly from each policy. Combines policies into the final behaviour.
- Policy classes — small classes (often templates) implementing one axis. Each policy has a defined interface (the policy interface) the host expects.
- Default policies — provided so users only override what they care about.
- Public inheritance — exposes policy members + allows users to call policy-specific methods via the host (Alexandrescu's choice; tradeable for composition).
3. C++ Example
A smart pointer with three orthogonal policies — ownership, checking, conversion:
#include <iostream>
#include <stdexcept>
// --- Ownership policy ---
template <typename T>
struct DefaultOwnership {
static T* clone(T* p) { return p ? new T(*p) : nullptr; }
static void destroy(T* p) { delete p; }
};
template <typename T>
struct SharedOwnership { // toy: refcount stored externally in real Loki
static T* clone(T* p) { return p; }
static void destroy(T* /*p*/) { /* refcount dec */ }
};
// --- Checking policy ---
template <typename T>
struct NoCheck {
static void onDeref(T*) {}
};
template <typename T>
struct AssertCheck {
static void onDeref(T* p) { if (!p) throw std::runtime_error("null deref"); }
};
// --- Host class ---
template <
typename T,
template <typename> class OwnershipPolicy = DefaultOwnership,
template <typename> class CheckingPolicy = NoCheck
>
class SmartPtr
: public OwnershipPolicy<T>
, public CheckingPolicy<T>
{
T* p_;
public:
explicit SmartPtr(T* p) : p_(p) {}
SmartPtr(const SmartPtr& other) : p_(OwnershipPolicy<T>::clone(other.p_)) {}
~SmartPtr() { OwnershipPolicy<T>::destroy(p_); }
T& operator*() { CheckingPolicy<T>::onDeref(p_); return *p_; }
T* operator->() { CheckingPolicy<T>::onDeref(p_); return p_; }
};
int main() {
SmartPtr<int> a{new int(1)}; // default, default
SmartPtr<int, SharedOwnership, AssertCheck> b{new int(2)}; // both overridden
std::cout << *a << " " << *b << "\n";
}
Behaviour combinatorics that would otherwise need M × N × P subclasses collapses to M + N + P policies.
4. C++20 with Concepts
Without concepts, breaking the policy contract → cryptic instantiation errors. C++20 lets you state the contract:
#include <concepts>
template <typename P, typename T>
concept OwnershipPolicy = requires(T* p) {
{ P::clone(p) } -> std::same_as<T*>;
{ P::destroy(p) } -> std::same_as<void>;
};
template <typename P, typename T>
concept CheckingPolicy = requires(T* p) {
{ P::onDeref(p) } -> std::same_as<void>;
};
template <
typename T,
template <typename> class OwnP = DefaultOwnership,
template <typename> class ChkP = NoCheck
>
requires OwnershipPolicy<OwnP<T>, T> && CheckingPolicy<ChkP<T>, T>
class SmartPtr : public OwnP<T>, public ChkP<T> { /* ... */ };
Errors now point at the violated concept, not the host's first failing line — same readability win concepts give SFINAE.
5. When to Use / When Not To
Use when:
- A class genuinely splits along orthogonal axes (allocator, threading model, error policy, storage).
- Performance matters: virtual-dispatch overhead is not acceptable.
- Combinatorics of variants would otherwise lead to inheritance explosion.
- You're writing a library; users will tweak some axes, accept defaults for the rest.
Avoid when:
- Behaviours interact strongly across axes (not actually orthogonal) — policies become a tangled mess.
- Variation must change at runtime — use Strategy with
std::functionor virtual. - Two or three axes max with no library users — plain composition or Strategy is simpler.
- You need binary stability — every policy change is an ABI change (different instantiation).
The stdlib uses this style heavily: std::basic_string<CharT, Traits, Allocator>, std::map<Key, Value, Compare, Allocator>, std::vector<T, Allocator>, std::priority_queue<T, Container, Compare>. Each template parameter is a policy.
6. Pitfalls
- Code bloat — every distinct combination instantiates a new class. Acceptable for a handful, expensive for hundreds.
- Compile time — template instantiation cost grows with policy count.
- Error messages — pre-concepts, errors are awful. C++20 concepts (§ 4) are the fix.
- Public inheritance — exposes policy methods on the host. Sometimes desirable, sometimes leaks abstraction. Private inheritance +
usingdeclarations is an alternative. - Empty Base Optimization — policies are usually empty classes; inheriting from many empty bases triggers EBO so they don't bloat object size. Multiple identical bases break EBO in pre-C++20 rules; C++20
[[no_unique_address]]+ composition is a modern escape hatch. - Diamond on shared sub-policies — policies that themselves inherit from a common base produce diamonds. Virtual inheritance fixes it but is rarely worth the cost; redesign instead.
- No runtime swap — the whole point is compile-time selection. If you find yourself wanting to pick at runtime, you wanted Strategy, not Policy-Based.
7. Related Patterns
- Policy-Based Design = compile-time Strategy — same separation of varying behaviour, different binding time. Strategy via composition + virtual dispatch swaps at runtime; Policy-Based via inheritance + template params resolves at compile time. See Strategy Pattern.
- CRTP — see CRTP. Often combined: a policy class uses CRTP to call back into the host. Both are C++ template idioms — CRTP gives static polymorphism via self-referential templates, Policy-Based composes behaviour via multiple template parameters.
- Mixin — overlap heavy. Mixins are usually CRTP-based; policies are usually inherited from. The line is fuzzy; both add behaviour via inheritance from template-parameter types.
- Traits —
std::char_traits,std::iterator_traits. Traits expose types and constants about another type; policies expose behaviour. Traits are usually free-standing class templates; policies are inherited. - Concepts (C++20) — express the contract a policy must satisfy. Don't replace Policy-Based Design; complement it (§ 4).
[[no_unique_address]]+ composition — modern alternative to public-inheritance-with-EBO for empty policies, lets the host hold the policy as a member instead of base.
8. References
- Alexandrescu, Modern C++ Design: Generic Programming and Design Patterns Applied, Addison-Wesley, 2001. Ch 1 (Policy-Based Class Design) — the canonical reference.
- Loki Library — Alexandrescu's reference implementation.
- Vandevoorde, Josuttis, Gregor. C++ Templates: The Complete Guide (2nd ed.) — policy chapter.
- cppreference: std::basic_string —
CharT,Traits,Allocatoras policies. - cppreference: std::map —
CompareandAllocatoras policies. [[no_unique_address]](C++20) — modern empty-policy storage.