I’ve been reading about templates a lot lately and kept seeing the word policy. Which made me realized I wanted a single post that answers the question: What is a policy? Why do C++ sources keep mentioning them? How do compile-time policies (templates) compare to runtime strategies (virtuals/std::function)? And when should which be used?
Quick summary
- A policy is a pluggable behavior a component delegates to (e.g., how to compare, allocate, log, back off, or lock) 1
- Compile-time policies (templates) give you static composition and zero-cost abstractions 2
- Runtime policies (virtual interfaces or type-erased callables) give you dynamic configurability 3
Is a “policy” just the Strategy pattern?
Close. Strategy is the OO runtime take; policy-based design usually implies compile-time composition. The idea—pluggable behavior—is the same.
Do policies exist without templates?
Yes. That’s exactly when the runtime strategy comes in to play.
What is a policy, really?
A policy is not a pattern on its own, but rather it’s a design technique used to separate what a component does from how it does it. You pass the how in.
Think of std::map<Key, T, Compare, Allocator>:
Similarly, std::unique_ptr<T, Deleter> uses a deleter policy, and std::unordered_map composes hash + equality policies 6. You don’t change the container’s “what”; you swap the “how”.
Compile-time policies
Compile-time policies are provided as template parameters, which are often stateless function objects. The compiler can inline calls and optimize everything away when possible.2.
Generate a policy:
// Comparator policy
struct Less
{
bool operator()(int a, int b) const noexcept
{
return a < b;
}
};
Use it in template:
// A holder that uses a comparator policy via object
template<typename Comp = Less>
struct MinOfTwo
{
int a{}, b{};
Comp comp{}; // Comparator policy object
int get() const
{
return comp(a, b) ? a : b;
}
};
Pros
- Zero/near-zero overhead via inlining 7
- Compile-time validation (with Concepts or
static_assert) - No virtual dispatch, no heap allocations
Cons
- Choices are fixed at compile time
- Many instantiations can increase code size
Validating policies with C++20 Concepts (nice errors!)
Using Less is valid in this Concept:
#include <concepts>
template<typename C>
concept Comparator = requires(C c, int a, int b) {
{ c(a, b) } -> std::convertible_to<bool>;
};
template<Comparator Comp = Less>
struct MinOfTwoChecked
{
int a{}, b{};
Comp comp{};
int get() const
{
return comp(a, b) ? a : b;
}
};
Misuse will fail at compile time with clear diagnostics. For example:
#include <concepts>
// Bad comparator policy
struct LessBAD
{
LessBAD operator()(int, int) const noexcept
{
return LessBAD{};
}
};
[[maybe_unused]] MinOfTwoChecked<LessBAD> minBAD{minValue, (minValue + 1)};
Gives out clear template constraint failure for template<class Comp> requires Comparator<Comp> -message when compiling. 8
Runtime policies
Runtime policies are provided via interfaces (virtual functions) or type erasure (e.g., std::function). This enables swapping of behavior based on configuration, user input, or plugins in runtime. 3
On this example, logging can be done via std::cout, when using CoutLog or silenced completely, if QuietLog pointer is provided:
// Strategy via interface
#include <iostream>
#include <memory>
// Interface
struct ILog
{
virtual ~ILog() = default;
virtual void log(const char*) = 0;
};
// Implementation for no logging
struct QuietLog: ILog
{
void log(const char*) override
{
}
};
// Implementation using std::cout
struct CoutLog: ILog
{
void log(const char* s) override
{
std::cout << s << "\n";
}
};
// User of strategy
class Service
{
public:
explicit Service(std::unique_ptr<ILog> log): log_(std::move(log))
{
}
void run()
{
log_->log("hello");
}
private:
std::unique_ptr<ILog> log_;
};
Pros
- Behavior configurable after deployment
- One code path (smaller binaries in some cases)
Cons
- Indirection (virtual call or type-erased thunk)
- Often heap allocation for the policy object
A middle ground
Instead of any of the above, pass a callable function (auto&& f) where it is needed:
// function that tries the operation until max_attempts is reached
// returns true if operation succeeds, otherwise false
bool retry_function(int max_attempts, auto&& operation)
{
for (int attempt = 1; attempt <= max_attempts; ++attempt)
{
if (operation())
{
return true;
}
}
return false;
}
You get flexibility without virtuals, and the compiler may inline the callable under optimization 9.
Head-to-head: when to use which (no middle ground here!)
Use compile-time policies when
- Performance is critical and the behavior set is stable
- Compile-time errors to prevent misuse (Concepts)
- Policies are stateless or trivially small
Use runtime policies when
- Behavior depends on configuration or external data
- You need plugin architectures or hot-swapping
- ABI stability is important (expose a stable virtual interface only)
A quick practical checklist
- Is the behavior stable and performance-critical? → Template policy
- Do you need to swap behavior at runtime? → Interface/
std::function - Do you need nice errors? → C++20 Concepts
- Worried about code size? → Consider runtime strategy or consolidate instantiations
Closing thoughts
Policies keep components orthogonal, composable, and often zero-cost. Whether you pick compile-time templates or runtime strategies depends on your constraints. Start with the simple rule: static when stable, dynamic when configurable. Please check libpolicy -library on my examples -repo.
-sorhanp
References
https://ahmedalsawi.github.io/posts/2025/09/c-policy-design-pattern/ ↩︎ ↩︎
https://www.geeksforgeeks.org/system-design/strategy-pattern-set-1/ ↩︎ ↩︎
https://stackoverflow.com/questions/5733254/how-can-i-create-my-own-comparator-for-a-map ↩︎
https://www.linkedin.com/pulse/custom-template-parameters-stl-allocators-comparators-sona-badalyan-tfxne ↩︎
https://iifx.dev/en/articles/457550510/boosting-c-performance-from-std-function-to-template-parameters ↩︎
https://www.geeksforgeeks.org/cpp/constraints-and-concepts-in-cpp-20/ ↩︎