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
- Template-Based Strategy (Compile-Time). Basic template strategy.
- Runtime Selection (Virtual Functions). Virtual function strategy.
- Traits vs. policies. Traits provide type metadata; policies provide behavior.
- Andrei Alexandrescu's "Modern C++ Design". The foundational work on Policy-Based Design.