Skip to content
C++
Idiom
since C++98
Advanced

Policy-Based Design

Inject behavior into class templates via policy parameters — compile-time Strategy with zero runtime overhead, composable across orthogonal concerns.

Policy-Based Designsince C++98

A host class accepts one or more template parameters — each a policy encapsulating one orthogonal behavioral axis — and inherits or composes them at compile time to produce a fully specialized type with no virtual dispatch and no runtime overhead.

Overview

Policy-based design is the compile-time form of the Strategy pattern. Where Strategy injects behavior through a virtual interface at runtime, policy-based design injects it through template parameters at compile time. The compiler sees the concrete policy, inlines the calls, and the resulting binary is indistinguishable from hand-written specialized code.

The core insight is decomposition into orthogonal concerns. A cache may have independent policies for storage, eviction, threading, and logging. A smart pointer may have independent policies for ownership, storage layout, and null checking. Each axis varies independently — six policy classes covering two threading options, three storage options, and two logging options yield twelve usable combinations instead of twelve concrete subclasses.

Policy-based design has been possible since C++98 but became significantly more ergonomic in C++11 with variadic templates, using type aliases, and deduced return types. C++17 added if constexpr for intra-policy compile-time branching. C++20's concepts finally provide a mechanism to express policy interface requirements explicitly, transforming inscrutable 40-line template errors into clear contract violations at the point of instantiation.

Syntax

A host class typically inherits privately from each policy. Private inheritance signals "implemented in terms of," not IS-A. Policies are not polymorphic base types.

cpp
template<
    typename StoragePolicy = InMemoryStorage,
    typename LoggingPolicy = NullLogger
>
class Cache : private StoragePolicy, private LoggingPolicy {
public:
    void set(std::string key, int value) {
        LoggingPolicy::log("set: " + key);        // inlined, zero overhead
        StoragePolicy::store(std::move(key), value);
    }

    std::optional<int> get(const std::string& key) {  // std::optional: C++17
        return StoragePolicy::fetch(key);
    }
};

Cache<MapStorage, ConsoleLogger> debug_cache;
Cache<HashStorage, NullLogger>   prod_cache;   // NullLogger adds 0 bytes via EBO

Empty Base Optimization

A stateless policy struct privately inherited collapses to zero bytes — the Empty Base Optimization (EBO). Composition via a member variable does not receive this treatment prior to C++20.

cpp
struct NullLogger {
    static void log(std::string_view) {}  // std::string_view: C++17
};

// Private inheritance: NullLogger contributes zero size
static_assert(sizeof(Cache<InMemoryStorage, NullLogger>) == sizeof(InMemoryStorage));

// Member variable: at least 1 byte for NullLogger, may cause padding
struct CacheByComposition {
    InMemoryStorage storage_;
    NullLogger      logger_;  // 1+ bytes regardless of content
};

C++20 introduced [[no_unique_address]], which grants member-based composition the same zero-size benefit:

cpp
struct CacheC20 {
    InMemoryStorage storage_;
    [[no_unique_address]] NullLogger logger_;  // zero size: C++20
};

Policy class destructors should be protected and non-virtual: protected so the host can destruct through the base without a vtable, non-virtual because accidental deletion through a policy pointer should be ill-formed.

Examples

Configurable object pool with independent threading and allocation policies

cpp
struct SingleThreaded {
    struct Lock { explicit Lock(SingleThreaded&) {} };
    Lock acquire() { return Lock{*this}; }
};

struct MultiThreaded {
    using Lock = std::lock_guard<std::mutex>;  // std::lock_guard: C++11
    Lock acquire() { return Lock{mutex_}; }
private:
    std::mutex mutex_;  // std::mutex: C++11
};

struct HeapAllocator {
    template<typename T, typename... Args>     // variadic templates: C++11
    T* create(Args&&... args) {
        return new T(std::forward<Args>(args)...);
    }
    template<typename T>
    void destroy(T* p) noexcept { delete p; }  // noexcept: C++11
};

template<
    typename T,
    typename ThreadPolicy = MultiThreaded,
    typename AllocPolicy  = HeapAllocator
>
class ObjectPool : private ThreadPolicy, private AllocPolicy {
public:
    template<typename... Args>
    T* acquire(Args&&... args) {
        auto lock = ThreadPolicy::acquire();
        return AllocPolicy::template create<T>(std::forward<Args>(args)...);
    }

    void release(T* p) noexcept {
        auto lock = ThreadPolicy::acquire();
        AllocPolicy::template destroy<T>(p);
    }
};

ObjectPool<Widget>                              prod_pool;   // full defaults
ObjectPool<Widget, SingleThreaded, HeapAllocator> test_pool; // no lock overhead

Adding a PoolAllocator or ArenaAllocator works for every threading combination without touching the host.

Constraining policies with concepts (C++20)

Without concepts, a misspelled method or wrong signature compiles silently until a confusing error fires inside the host. Concepts promote the diagnostic to the call site.

cpp
// C++20
template<typename T>
concept LoggerPolicy = requires(T t, std::string_view msg) {
    { t.log(msg) } -> std::same_as<void>;
};

template<typename T>
concept AllocatorPolicy = requires(T t, std::size_t n, void* p) {
    { t.allocate(n) }      -> std::convertible_to<void*>;
    { t.deallocate(p, n) };
};

template<LoggerPolicy Log, AllocatorPolicy Alloc>
class Service : private Log, private Alloc {
    // Concept violations reported here, not 30 frames deep
};

For C++11/14/17, use static_assert with type traits as a fallback:

cpp
// C++11 fallback
template<typename Log, typename Alloc>
class Service : private Log, private Alloc {
    static_assert(std::is_invocable_v<decltype(&Log::log), Log, std::string_view>,
                  "Log policy must provide void log(std::string_view)");  // C++17
};

Policy bundle via traits class

When policies cluster together or share type dependencies, a traits class prevents parameter-count explosion.

cpp
struct ProductionTraits {
    using Logger    = FileLogger;
    using Allocator = PoolAllocator;
    using Hasher    = std::hash<std::string>;  // std::hash: C++11
};

struct TestTraits {
    using Logger    = NullLogger;
    using Allocator = HeapAllocator;
    using Hasher    = std::hash<std::string>;
};

template<typename Traits = ProductionTraits>
class Index
    : private Traits::Logger
    , private Traits::Allocator
{
    using Hasher = typename Traits::Hasher;
    // ...
};

Index<>           prod_index;
Index<TestTraits> test_index;  // swap entire profile atomically

This mirrors the std::char_traits and allocator model used throughout the standard library.

Best Practices

One policy per orthogonal concern. If two parameters always change together, merge them into one policy class or a traits bundle. If they are truly independent, keep them as separate parameters so they compose freely.

Private inheritance, protected destructor. The host IS-NOT-A policy. Inherit privately. Give policy classes a protected non-virtual destructor: the host destructs correctly, but delete policy_ptr is ill-formed.

Provide sensible defaults. Most call sites should work with bare Cache<>. Default policies should be production-correct, not test doubles. Reserve explicit parameters for the exceptional case.

Prefer stateless policies. The more state a policy carries, the more it resembles a strategy object — at which point runtime polymorphism is often the simpler, clearer design. Compile-time policies deliver maximum value when they are pure algorithms or zero-overhead wrappers.

Enforce the interface contract. Use concepts (C++20) or static_assert + type traits (C++11) to document and check what a valid policy must provide. Never leave the contract implicit.

Common Pitfalls

Combinatorial explosion at link time. Four policies with three options each yields 81 distinct instantiations. If all are instantiated in a single translation unit, compile and link times suffer. Use extern template (C++11) to suppress implicit instantiation in widely-included headers, and instantiate explicitly in one .cpp.

Policy coupling. A StoragePolicy that must know about LoggingPolicy types creates hidden inter-policy dependencies. Keep policies ignorant of each other; let the host mediate any shared types.

Forgetting EBO. Using StoragePolicy storage_ as a data member instead of inheriting adds at least one byte — often more after alignment padding — for every empty policy. This matters in containers that store an allocator alongside their data. Use private inheritance or [[no_unique_address]] (C++20).

No policy validation. Without constraints, a wrong policy silently compiles until a substitution failure produces a wall of template instantiation trace. Always add concept requirements (C++20) or static_assert guards (C++11+) before the first user reports a cryptic error.

Origins: Alexandrescu, Loki, and "Modern C++ Design"

Policy-based design was named and systematized by Andrei Alexandrescu in his 2001 book Modern C++ Design: Generic Programming and Design Patterns Applied. The book introduced the Loki library — a header-only toolkit of generic components built entirely from composable policies — and showed that the technique could replace entire families of hand-written classes with a single parameterized host.

The canonical showcase from Modern C++ Design is SmartPtr, a smart pointer parameterized on four orthogonal behavioral axes:

cpp
// Loki SmartPtr signature (simplified from Modern C++ Design, Chapter 7)
template<
    typename T,
    template<class> class OwnershipPolicy   = RefCounted,
    class ConversionPolicy                  = DisallowConversion,
    template<class> class CheckingPolicy    = AssertCheck,
    template<class> class StoragePolicy     = DefaultSPStorage
>
class SmartPtr
    : public StoragePolicy<T>
    , public OwnershipPolicy<typename StoragePolicy<T>::PointerType>
    , public CheckingPolicy<typename StoragePolicy<T>::PointerType>
    , public ConversionPolicy
{
    // ...
};

Each parameter is independently swappable:

  • OwnershipPolicy — RefCounted, RefCountedMT, DeepCopy, NoCopy, COMRefCounted
  • ConversionPolicy — AllowConversion, DisallowConversion
  • CheckingPolicy — AssertCheck, RejectNullStatic, NoCheck
  • StoragePolicy — DefaultSPStorage, LockedStorage, ArrayStorage

Five policies with two to five options each produce over sixty distinct smart pointer variants — reference-counted thread-safe pointer with null rejection, a deep-copying non-converting pointer, a COM pointer, and so on — without writing sixty concrete classes. Alexandrescu coined the term policy class to distinguish compile-time template parameters that carry behavior from the runtime Strategy objects of Gang-of-Four design.

The inheritance structure in SmartPtr is intentionally public for the storage and conversion policies so that the smart pointer can participate in implicit conversions and expose the raw pointer type. Later refinements by the community moved most policy inheritance to private, using using declarations to selectively expose members — a practice reflected in the ## Syntax section of this page.

Loki is still available as open source. The ideas it introduced — notably the TypeList for type-level sequences and policy-based generic components — directly influenced std::tuple, std::variant, and the design of the C++11 type traits library.

Multiple Inheritance of Policy Classes and EBO

When a host inherits from several policy classes simultaneously, two properties determine whether it remains zero-overhead: the Empty Base Optimization and the lack of a vtable.

How EBO works

The C++ standard permits a compiler to assign zero size to an empty base subobject (one with no non-static data members and no virtual functions) so that it does not consume space inside the derived object. Practically, all major compilers (GCC, Clang, MSVC) apply EBO to private bases:

cpp
struct NullMutex {
    void lock()   noexcept {}
    void unlock() noexcept {}
};

struct RealMutex {
    void lock();
    void unlock();
private:
    pthread_mutex_t m_;  // 40 bytes on Linux
};

template<typename T, typename MutexPolicy = NullMutex>
class Queue : private MutexPolicy {
    std::deque<T> data_;
public:
    void push(T v) {
        MutexPolicy::lock();
        data_.push_back(std::move(v));
        MutexPolicy::unlock();
    }
};

// EBO collapses NullMutex base to zero bytes
static_assert(sizeof(Queue<int, NullMutex>) == sizeof(std::deque<int>));
// RealMutex adds its pthread_mutex_t to the total
static_assert(sizeof(Queue<int, RealMutex>) > sizeof(std::deque<int>));

The result is a single binary Queue template that compiles to lock-free code when NullMutex is chosen and to correct mutex-protected code when RealMutex is chosen — with no branching, no virtual dispatch, and no wasted bytes.

Combining multiple empty policies

Multiple empty policy bases are all collapsed together. The host's size equals the sum of non-empty bases plus its own data members:

cpp
struct NoLog     { static void log(std::string_view) noexcept {} };
struct NoMetrics { static void record(std::string_view) noexcept {} };

template<typename T,
         typename LogPolicy     = NoLog,
         typename MetricPolicy  = NoMetrics,
         typename MutexPolicy   = NullMutex>
class Store
    : private LogPolicy
    , private MetricPolicy
    , private MutexPolicy
{
    std::unordered_map<std::string, T> map_;
public:
    void insert(std::string key, T val) {
        MutexPolicy::lock();
        LogPolicy::log("insert: " + key);
        MetricPolicy::record("store.insert");
        map_.emplace(std::move(key), std::move(val));
        MutexPolicy::unlock();
    }
};

// All three defaults are empty — sizeof equals sizeof(unordered_map<string,T>)
Store<int> prod;

// Only MetricPolicy is non-empty here (hypothetical StatsdMetrics has a socket fd)
// Store<int, NoLog, StatsdMetrics, NullMutex> has StatsdMetrics size added

Protected non-virtual destructors in policy classes

Because the host inherits privately, external code cannot hold a PolicyBase* pointing to a Host object. But to be safe — and to make delete this in a policy method a compile error — policy classes should declare a protected non-virtual destructor:

cpp
struct RefCounted {
    void add_ref() noexcept { ++count_; }
    bool release() noexcept { return --count_ == 0; }
protected:
    ~RefCounted() = default;  // protected: host destructs; external delete is ill-formed
private:
    int count_{1};
};

Policy-Based Design vs the Strategy Pattern

Both idioms inject swappable behavior into a host, but they sit at opposite ends of the static/dynamic spectrum.

DimensionPolicy-based designStrategy pattern
Binding timeCompile timeRuntime
Dispatch costZero (inlined)Virtual call (~ns)
Binary sizeOne instantiation per policy setOne class, one vtable
Heterogeneous containersNot directlyYes — vector&lt;unique_ptr&lt;Strategy&gt;&gt;
Hot-swap at runtimeNoYes
Error messagesTemplate instantiation errors (improved by concepts)Clean at definition site
Typical usePerformance-critical paths, library primitivesPluggable user-extensible behavior

The rule of thumb: choose policies when the behavioral variant is known at compile time and either performance or code-size matters. Choose runtime strategy when the variant is chosen at runtime — configuration files, user input, plugin systems — or when you need to store heterogeneous strategy objects in the same container.

A hybrid is also valid: a policy class can internally use runtime polymorphism. std::pmr::polymorphic_allocator (below) is exactly this: it is selected as a compile-time policy parameter, but it dispatches allocation calls at runtime through a memory_resource pointer.

std::allocator as a Policy: The Standard Library's Canonical Example

The C++ standard library has used policy-based design since C++98. Every container in &lt;vector&gt;, &lt;list&gt;, &lt;map&gt;, and &lt;unordered_map&gt; accepts an allocator as its final template parameter:

cpp
// From the standard: std::vector is a policy-based host
template<typename T, typename Allocator = std::allocator<T>>
class vector { /* ... */ };

// Custom pool allocator dropped in without changing any vector code
std::vector<int, PoolAllocator<int>> v;

The allocator policy controls:

  • how raw memory is obtained (allocate / deallocate)
  • how objects are constructed and destroyed in that memory (construct / destroy)
  • propagation behavior when containers are copied, moved, or swapped (propagate_on_container_*)

std::allocator<T> is stateless, so EBO applies: sizeof(std::vector<int>) equals three pointers — begin, end, capacity — with zero bytes for the allocator.

std::basic_string — three policy parameters

std::string is a typedef for std::basic_string with three policies:

cpp
// The full declaration of std::basic_string
template<
    typename CharT,                            // character type policy
    typename Traits = std::char_traits<CharT>, // character operations policy
    typename Allocator = std::allocator<CharT> // memory management policy
>
class basic_string { /* ... */ };

using string    = basic_string<char>;
using wstring   = basic_string<wchar_t>;
using u8string  = basic_string<char8_t>;   // C++20
using u16string = basic_string<char16_t>;
using u32string = basic_string<char32_t>;

std::char_traits is itself a policy that defines character comparison, copy, and search operations. Replacing it allows case-insensitive strings or strings over custom character encodings — without touching basic_string itself:

cpp
struct CaseInsensitiveTraits : std::char_traits<char> {
    static bool eq(char a, char b) noexcept {
        return std::tolower(a) == std::tolower(b);
    }
    static bool lt(char a, char b) noexcept {
        return std::tolower(a) < std::tolower(b);
    }
    static int compare(const char* s1, const char* s2, std::size_t n) {
        for (std::size_t i = 0; i < n; ++i) {
            if (lt(s1[i], s2[i])) return -1;
            if (lt(s2[i], s1[i])) return +1;
        }
        return 0;
    }
};

using IString = std::basic_string<char, CaseInsensitiveTraits>;

IString a{"Hello"}, b{"hello"};
assert(a == b);  // true — comparison uses CaseInsensitiveTraits::compare

std::pmr::polymorphic_allocator — a hybrid policy

C++17 introduced std::pmr::polymorphic_allocator, which demonstrates how a compile-time policy can embed runtime dispatch:

cpp
#include <memory_resource>

// Compile-time policy selection: pmr::vector uses polymorphic_allocator<T>
std::pmr::vector<int> v;  // same as vector<int, pmr::polymorphic_allocator<int>>

// Runtime selection of the underlying memory_resource
std::array<std::byte, 4096> buf;
std::pmr::monotonic_buffer_resource pool{buf.data(), buf.size()};

// Swap memory resources at runtime — no template parameter change
std::pmr::vector<int> arena_v{&pool};

The type std::pmr::vector<int> is fixed at compile time, but the underlying allocation strategy — monotonic arena, synchronized pool, or heap — is chosen at runtime through the memory_resource virtual interface. This pattern is useful when you need runtime flexibility inside a compile-time-typed API: the vector's type remains concrete and cache-friendly, but the allocator adapts.

C++20 Concepts as Policy Constraints

Before C++20, a policy that provided logg() instead of log() or used the wrong signature would only fail when the host template was instantiated, often producing dozens of cascading errors from deep inside the template hierarchy. Concepts relocate the failure to the point where the policy is bound to the host.

Defining policy concepts

cpp
// C++20
template<typename P>
concept LockPolicy = requires(P p) {
    { p.lock()   } -> std::same_as<void>;
    { p.unlock() } -> std::same_as<void>;
};

template<typename P>
concept AllocPolicy = requires(P p, std::size_t n, void* ptr) {
    { p.allocate(n)      } -> std::convertible_to<void*>;
    { p.deallocate(ptr, n) };
};

template<typename P, typename T>
concept StoragePolicy = requires(P p, std::string_view key) {
    { p.store(key, std::declval<T>()) };
    { p.fetch(key) } -> std::same_as<std::optional<T>>;
};

Applying concepts to a host

cpp
template<
    typename T,
    LockPolicy   Mutex   = NullMutex,
    AllocPolicy  Alloc   = std::allocator<T>
>
    requires StoragePolicy<MapStorage<T>, T>  // demonstrate compound constraint
class Cache : private Mutex, private Alloc {
public:
    std::optional<T> get(std::string_view key);
    void             set(std::string_view key, T val);
};

// Violation caught immediately at the host declaration, not inside it:
// Cache<int, int> bad;
// ^ error: 'int' does not satisfy 'LockPolicy'
//   note: 'int::lock()' not found

Concepts vs SFINAE for policy constraints

Prior to C++20, the equivalent guard used std::enable_if or static_assert with type traits:

cpp
// C++17 equivalent — verbose and gives worse diagnostics
template<typename Mutex, typename Alloc>
class Cache : private Mutex, private Alloc {
    static_assert(
        std::is_member_function_pointer_v<decltype(&Mutex::lock)>,
        "Mutex policy must provide a lock() member function"
    );
    static_assert(
        std::is_same_v<
            decltype(std::declval<Alloc>().allocate(std::size_t{})),
            void*
        >,
        "Alloc policy must provide void* allocate(size_t)"
    );
};

C++20 concepts are strictly superior: the constraint is named and reusable, the error points to the instantiation site rather than inside the static_assert, and the requirement is visible in auto-generated documentation and IDE tooling.

Policy-Based Design in the Standard Library

Policy-based design is not a niche technique. It underpins several core standard library classes:

std::basic_ios and std::basic_iostream — parameterized on CharT and Traits, with the stream buffer as a runtime strategy component. The Traits policy defines end-of-file, character comparison, and integer type — enabling both narrow and wide streams from a single class template.

std::basic_regex — accepts a Traits policy controlling locale-aware character classification. std::regex_traits<char> is the default; custom traits enable case-folding or Unicode-aware matching.

std::hash specializations — std::unordered_map<K, V, Hash, KeyEqual> takes Hash and KeyEqual as separate policies. Replacing Hash with a cryptographic hash or a perfect hash for a closed key set changes performance characteristics without modifying unordered_map.

std::function vs std::move_only_function (C++23) — these are runtime strategy containers, not compile-time policies, but the contrast is instructive. std::function<Sig> erases callable type at runtime; a policy template template&lt;Callable C&gt; class Task : private C binds at compile time. Use std::function when the callable is determined at runtime; use a policy when it is a template parameter.

std::basic_format_string (C++20) — the format string type is parameterized on CharT, making the formatting machinery a policy over character type. This is the same structural decision as basic_string — one template, four instantiations, zero duplicated logic.

See Also

  • CRTP — policy-based design and CRTP compose naturally; policies often use CRTP to call back into the host
  • Type Erasure — runtime counterpart; prefer when policy selection cannot be resolved at compile time
  • Templates — foundational mechanism for all policy machinery
  • Concepts — C++20 mechanism for expressing and statically enforcing policy interface requirements