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>:

  • Compare is a comparison policy (e.g., std::less<Key>) 4
  • Allocator is a memory policy 5

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