Policy Based Design (PBD) combines C++ templates with Strategy. A policy is a template parameter that supplies one behavior, such as sorting, logging, or memory allocation. The compiler resolves the policy, so the abstraction costs nothing at runtime.

Core concept

A policy is a template parameter that defines a specific behavior or algorithm. Traditional template parameters are usually concrete types; policies are templates or classes that define a set of operations or an interface. The context class (the host) uses these policies to customize its behavior:

// Policy 1: Sorting strategy
struct QuickSortPolicy {
    template<typename T>
    void sort(std::vector<T>& data) const {
        std::sort(data.begin(), data.end());
    }
};

struct MergeSortPolicy {
    template<typename T>
    void sort(std::vector<T>& data) const {
        std::stable_sort(data.begin(), data.end());
    }
};

// Policy 2: Logging behavior
struct NoLoggingPolicy {
    void log(const std::string& msg) const {}
};

struct FileLoggingPolicy {
    void log(const std::string& msg) const {
        // Log to file
        std::ofstream file("debug.log", std::ios::app);
        file << msg << "\n";
    }
};

// Host class: customized by policies
template<typename SortPolicy, typename LoggingPolicy>
class DataProcessor {
private:
    SortPolicy sort_policy;
    LoggingPolicy log_policy;
    std::vector<int> data;

public:
    void add_data(int value) {
        data.push_back(value);
    }

    void process() {
        log_policy.log("Starting sort...");
        sort_policy.sort(data);
        log_policy.log("Sort complete");
    }

    void print() const {
        for (int val : data) {
            std::cout << val << " ";
        }
        std::cout << "\n";
    }
};

// Usage: compose behavior at compile-time
int main() {
    // Fast processing with no logging
    DataProcessor<QuickSortPolicy, NoLoggingPolicy> fast_processor;
    fast_processor.add_data(30);
    fast_processor.add_data(10);
    fast_processor.add_data(20);
    fast_processor.process();
    fast_processor.print();

    // Stable sorting with detailed logging
    DataProcessor<MergeSortPolicy, FileLoggingPolicy> debug_processor;
    debug_processor.add_data(30);
    debug_processor.add_data(10);
    debug_processor.add_data(20);
    debug_processor.process();  // Logs to file
    debug_processor.print();

    return 0;
}

Anatomy of a policy

A policy defines a contract (an interface) that the host class expects:

// Example: Sorting Policy interface
struct SortingPolicy {
    // Must be callable (functor or function pointer)
    // or have a member function like:
    template<typename Container>
    void sort(Container& data) const;
};

// Example: Hashing Policy interface
struct HashingPolicy {
    template<typename T>
    size_t hash(const T& value) const;
};

// Example: Allocation Policy interface
struct MemoryAllocationPolicy {
    void* allocate(size_t size);
    void deallocate(void* ptr);
};

Policies are typically:

  • Stateless (or nearly so). Policies are usually thin algorithm containers.
  • Template-aware. They work with template parameters.
  • Concept-based. They define an implicit interface rather than explicit inheritance.

Comparison: policies vs. traits

Traits provide type information and metadata:

template<typename T>
struct numeric_traits {
    static constexpr T max_value() { return std::numeric_limits<T>::max(); }
    static constexpr T min_value() { return std::numeric_limits<T>::min(); }
};

Policies provide behavior and algorithms:

struct SaturatingArithmetic {
    template<typename T>
    T add(T a, T b) const {
        // Saturate instead of overflow
        if (a > std::numeric_limits<T>::max() - b) {
            return std::numeric_limits<T>::max();
        }
        return a + b;
    }
};

struct WrapAroundArithmetic {
    template<typename T>
    T add(T a, T b) const {
        // Traditional wrap-around on overflow
        return a + b;
    }
};

Real-world example: smart container

// Storage policy
struct VectorStoragePolicy {
    template<typename T>
    using container = std::vector<T>;
};

struct DequeStoragePolicy {
    template<typename T>
    using container = std::deque<T>;
};

// Thread safety policy
struct SingleThreadPolicy {
    void lock() const {}
    void unlock() const {}
};

struct MutexThreadPolicy {
    mutable std::mutex mtx;
    void lock() const { mtx.lock(); }
    void unlock() const { mtx.unlock(); }
};

// Growth policy
struct ExponentialGrowthPolicy {
    static constexpr float growth_factor() { return 1.5f; }
};

struct LinearGrowthPolicy {
    static constexpr float growth_factor() { return 0.25f; }
};

// Policy-based container
template<typename T, 
         typename StoragePolicy = VectorStoragePolicy,
         typename ThreadPolicy = SingleThreadPolicy,
         typename GrowthPolicy = ExponentialGrowthPolicy>
class SmartContainer {
private:
    typename StoragePolicy::template container<T> data;
    ThreadPolicy thread_policy;
    GrowthPolicy growth_policy;

public:
    void push_back(const T& value) {
        thread_policy.lock();
        data.push_back(value);
        if (data.size() >= data.capacity() * 0.8f) {
            reserve(static_cast<size_t>(
                data.capacity() * growth_policy.growth_factor()
            ));
        }
        thread_policy.unlock();
    }

    void reserve(size_t new_capacity) {
        // Reserve implementation using storage policy
    }

    size_t size() const {
        return data.size();
    }
};

// Usage: different configurations
int main() {
    // Single-threaded with vector, exponential growth
    SmartContainer<int> fast_container;

    // Multi-threaded with deque, linear growth
    SmartContainer<int, 
                   DequeStoragePolicy, 
                   MutexThreadPolicy, 
                   LinearGrowthPolicy> safe_container;

    fast_container.push_back(42);
    safe_container.push_back(99);

    return 0;
}

Advantages of policy-based design

Advantage Explanation
Zero-cost abstraction Policies are resolved at compile time, so there is no runtime overhead
Flexibility Combine policies in any way; inheritance does not limit the combinations
Code reuse Mix and match independent behaviors
Type safety All decisions happen at compile time
No virtual functions Direct function calls and full inlining
Control over binary size Only the combinations you use are instantiated
Orthogonal concerns Policies handle independent aspects
Framework creation Build extensible libraries without inheritance hierarchies

Disadvantages of policy-based design

Disadvantage Explanation
Compilation time Heavy template use increases compile time significantly
Template code bloat Each policy combination generates new code
Complex error messages Template errors can be cryptic and hard to debug
Steep learning curve Requires advanced C++ knowledge and experience
Limited runtime flexibility Policies must be fixed at compile time
IDE support Code completion and refactoring work poorly with templates
Debugging difficulty Stepping through generated template code is painful
Documentation burden Implicit interfaces require extensive documentation

Policy-based library: the Loki approach

The Loki C++ library uses Policy-Based Design:

// From Loki (simplified example)
template<typename T,
         template<class> class CreationPolicy = OpNewCreator,
         template<class> class LifetimePolicy = DefaultLifetime,
         template<class, class> class ThreadingPolicy = SingleThreaded>
class SmartPtr {
    // Implementation using policies
};

// Usage: customize SmartPtr behavior
SmartPtr<MyClass,             // Type
         OpNewCreator,        // How to create
         DefaultLifetime,     // How to manage lifetime
         SingleThreaded>      // Thread safety
my_ptr;

// For multi-threaded use:
SmartPtr<MyClass,
         OpNewCreator,
         DefaultLifetime,
         ObjectLevelLocking>  // Different threading policy
thread_safe_ptr;

When to use policy-based design

Good use cases:

  • Reusable library code with independent variations
  • Multiple independent customization axes, such as storage, threading, and growth
  • Zero-cost abstraction is critical
  • All policy combinations are known at compile time
  • Framework or library development

Avoid it when:

  • You have only one or two customization points (templates are overkill)
  • You need runtime algorithm selection
  • The team has limited experience with templates
  • Compilation time is constrained
  • Simple inheritance would suffice

Comparing strategy approaches

Aspect Template-Based Policy-Based Runtime-Selected
Selection time Compile-time Compile-time Runtime
Overhead Zero Zero 1-3 cycles (vtable)
Customization axes 1-2 Many (orthogonal) Single (polymorphism)
Compile time Fast Slow (heavy templates) Fast
Binary size Moderate Can be large Small
Runtime flexibility None None Full
Type safety Strong Strong Weak
Learning curve Easy Hard Easy
IDE support Good Poor Good
Suited for Simple algorithms Library frameworks App-level code

Example: database connection pooling

// Policies for customization
struct FIFOQueuePolicy {
    template<typename T>
    using container = std::queue<T>;
};

struct LIFOStackPolicy {
    template<typename T>
    using container = std::stack<T>;
};

struct NoValidationPolicy {
    void validate(const Connection& conn) const {}
};

struct PingValidationPolicy {
    void validate(const Connection& conn) const {
        conn.ping();  // Verify connection is alive
    }
};

// Policy-based connection pool
template<typename QueuePolicy, typename ValidationPolicy>
class ConnectionPool {
private:
    typename QueuePolicy::template container<Connection> available;
    ValidationPolicy validator;

public:
    Connection get_connection() {
        if (available.empty()) {
            return Connection::create();
        }

        auto conn = available.front();  // or top() for LIFO
        available.pop();

        validator.validate(conn);  // May be no-op depending on policy
        return conn;
    }

    void return_connection(Connection conn) {
        available.push(conn);
    }
};

// Configurations
using FastPool = ConnectionPool<FIFOQueuePolicy, NoValidationPolicy>;
using SafePool = ConnectionPool<LIFOStackPolicy, PingValidationPolicy>;

Summary

Policy-Based Design lets a library expose several independent customization points without runtime cost. It works because each policy is a type the compiler can see through. The bill arrives at compile time: more instantiations, longer builds, and error messages that can be hard to read. Use PBD for libraries and frameworks. Application code usually does better with the simpler Strategy forms.

See also