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.

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