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

  • 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::function or 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 + using declarations 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.
  • Traitsstd::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_stringCharT, Traits, Allocator as policies.
  • cppreference: std::mapCompare and Allocator as policies.
  • [[no_unique_address]] (C++20) — modern empty-policy storage.