Skip to content
C++
Idiom
since C++98
Basic

Strategy Pattern

Swap algorithms at runtime or compile time without touching the context. Virtual dispatch, std::function, template policies, and C++20 concepts compared.

Strategy Patternsince C++98

Encapsulates an interchangeable algorithm behind a stable interface so the owning context can switch implementations at runtime or compile time without modification.

Overview

Strategy solves a problem at two distinct points in time. At runtime, the context holds a handle to whichever algorithm is currently active and can swap it mid-flight. At compile time, a template parameter selects the algorithm, giving the compiler full visibility to inline and optimise across the boundary.

The pattern appears wherever the what is fixed but the how must vary: compression codec, network transport, sorting order, logging sink, pricing rule, authentication method. The key design question is not whether to use strategy but which mechanism to use, because C++ offers at least four with meaningfully different trade-offs.

Approaches

Virtual dispatch (C++98)

cpp
struct SortStrategy {
    virtual void sort(std::vector<int>&) const = 0;
    virtual ~SortStrategy() = default;      // mandatory β€” omitting causes UB on delete
};

struct QuickSort : SortStrategy {
    void sort(std::vector<int>& v) const override {
        std::sort(v.begin(), v.end());
    }
};

struct StableSort : SortStrategy {
    void sort(std::vector<int>& v) const override {
        std::stable_sort(v.begin(), v.end());
    }
};

class Sorter {
    std::unique_ptr<SortStrategy> strategy_;   // unique_ptr: C++11
public:
    explicit Sorter(std::unique_ptr<SortStrategy> s) : strategy_{std::move(s)} {}

    void set_strategy(std::unique_ptr<SortStrategy> s) { strategy_ = std::move(s); }

    void sort(std::vector<int>& v) const { strategy_->sort(v); }
};

Sorter s{std::make_unique<QuickSort>()};
s.sort(data);
s.set_strategy(std::make_unique<StableSort>());
s.sort(data);

Virtual dispatch costs one indirect call per invocation and prevents the compiler from reasoning across the call boundary. The cost is usually immaterial against non-trivial algorithm work, but it matters in tight loops.

std::function (C++11)

Accepts lambdas, function pointers, and any callable β€” no inheritance hierarchy required:

cpp
#include <functional>  // C++11

using SortFn = std::function<void(std::vector<int>&)>;

class Sorter {
    SortFn strategy_;
public:
    explicit Sorter(SortFn fn) : strategy_{std::move(fn)} {}
    void set_strategy(SortFn fn) { strategy_ = std::move(fn); }
    void sort(std::vector<int>& v) const { strategy_(v); }
};

Sorter s{[](std::vector<int>& v){ std::sort(v.begin(), v.end()); }};

s.set_strategy([](std::vector<int>& v){
    std::sort(v.begin(), v.end(), std::greater<int>{});  // C++11 std::greater<int>
});

Internally std::function applies type erasure. All major implementations include small buffer optimisation (SBO): callables whose closure fits within roughly 16–32 bytes (implementation-defined) are stored in-place without heap allocation. Lambdas that capture a shared_ptr, a string, or several scalars often exceed the threshold and trigger a heap allocation on assignment.

std::function requires the stored callable to be copyable. A lambda capturing a unique_ptr or an ofstream will not compile. C++23 adds std::move_only_function for this case:

cpp
// C++23
#include <functional>

std::move_only_function<void(std::vector<int>&)> strategy =
    [resource = std::make_unique<SortContext>()](std::vector<int>& v) mutable {
        resource->sort(v);
    };

Template policy (C++98, default template args on classes: C++98, on functions: C++11)

Zero runtime overhead β€” the algorithm is selected at instantiation and inlined:

cpp
struct AscendingPolicy {
    static void sort(std::vector<int>& v) { std::sort(v.begin(), v.end()); }
};

struct DescendingPolicy {
    static void sort(std::vector<int>& v) {
        std::sort(v.begin(), v.end(), std::greater<int>{});
    }
};

struct StableAscendingPolicy {
    static void sort(std::vector<int>& v) { std::stable_sort(v.begin(), v.end()); }
};

template<typename Policy = AscendingPolicy>
class DataPipeline {
    std::vector<int> data_;
public:
    void ingest(std::vector<int> v) { data_ = std::move(v); }
    void process() {
        // ... transform ...
        Policy::sort(data_);    // inlined at compile time, zero call overhead
    }
    const std::vector<int>& result() const { return data_; }
};

DataPipeline<AscendingPolicy>        fast;
DataPipeline<StableAscendingPolicy>  stable;

The cost: each distinct Policy produces a distinct type. You cannot store DataPipeline<AscendingPolicy> and DataPipeline<DescendingPolicy> in the same std::vector without another layer of type erasure.

Concept-constrained strategy (C++20)

Concepts document the expected interface and replace inscrutable substitution-failure diagnostics with actionable constraint errors:

cpp
// C++20
template<typename S>
concept SortStrategy = requires(S s, std::vector<int>& v) {
    { s.sort(v) } -> std::same_as<void>;
};

template<SortStrategy S>
class Pipeline {
    S sorter_;
public:
    explicit Pipeline(S s) : sorter_{std::move(s)} {}
    void run(std::vector<int>& v) { sorter_.sort(v); }
};

struct RadixSort {
    void sort(std::vector<int>& v) {
        if (v.empty()) return;
        int mx = *std::max_element(v.begin(), v.end());
        for (int exp = 1; mx / exp > 0; exp *= 10) {
            std::vector<int> out(v.size());
            int cnt[10]{};
            for (int x : v) ++cnt[(x / exp) % 10];
            for (int i = 1; i < 10; ++i) cnt[i] += cnt[i-1];
            for (int i = (int)v.size()-1; i >= 0; --i)
                out[--cnt[(v[i] / exp) % 10]] = v[i];
            v = std::move(out);
        }
    }
};

Pipeline<RadixSort> p{RadixSort{}};
p.run(data);

Examples

Logging sink with runtime swap

cpp
#include <functional>
#include <string_view>   // C++17
#include <iostream>
#include <fstream>
#include <memory>

class Logger {
public:
    using Sink = std::function<void(std::string_view)>;

    explicit Logger(Sink sink) : sink_{std::move(sink)} {}
    void set_sink(Sink s) { sink_ = std::move(s); }
    void log(std::string_view msg) const { sink_(msg); }

private:
    Sink sink_;
};

// start with console
Logger logger{[](std::string_view msg){ std::cout << msg << '\n'; }};

// rotate to file at runtime β€” shared_ptr keeps the stream alive in the lambda
auto file = std::make_shared<std::ofstream>("app.log", std::ios::app);
logger.set_sink([file](std::string_view msg){ *file << msg << '\n'; });

Codec selection via compile-time policy

When the codec is fixed per build target (embedded vs. server), template policy eliminates dispatch overhead entirely:

cpp
#include <cstddef>
#include <span>    // C++20
#include <vector>

struct ZstdCodec {
    static std::vector<std::byte> compress(std::span<const std::byte> in);
    static std::vector<std::byte> decompress(std::span<const std::byte> in);
};

struct NoopCodec {
    static std::vector<std::byte> compress(std::span<const std::byte> in) {
        return {in.begin(), in.end()};
    }
    static std::vector<std::byte> decompress(std::span<const std::byte> in) {
        return {in.begin(), in.end()};
    }
};

template<typename Codec = ZstdCodec>
class StorageEngine {
public:
    void write(std::span<const std::byte> data) {
        auto blob = Codec::compress(data);
        // write blob to disk
    }
    std::vector<std::byte> read(std::span<const std::byte> raw) {
        return Codec::decompress(raw);
    }
};

#ifdef TARGET_EMBEDDED
using Engine = StorageEngine<NoopCodec>;
#else
using Engine = StorageEngine<ZstdCodec>;
#endif

Best Practices

  • Default to std::function for runtime strategies. The SBO miss and one indirect call are negligible compared to most real algorithm bodies. Only switch to templates after profiling.
  • Inject the strategy through the constructor. Constructor injection makes the dependency visible in the type, simplifies testing with mock strategies, and prevents the context from ever existing in an indeterminate state. Reserve set_strategy setters for explicit hot-swap scenarios.
  • Apply C++20 concepts to template strategies. They serve as machine-checked documentation of the required interface and produce constraint not satisfied errors instead of pages of substitution failures.
  • Keep strategy interfaces narrow. One or two virtual methods is the norm. A wide interface suggests the strategy is doing too much β€” split it.
  • Prefer std::move_only_function (C++23) when a strategy must own non-copyable resources. Contorting ownership to satisfy std::function's copyability requirement produces subtle bugs.

Common Pitfalls

Missing virtual destructor. Deleting a unique_ptr<Base> without a virtual destructor in Base is undefined behaviour β€” the derived destructor is never called. Every polymorphic base requires virtual ~Base() = default.

SBO surprise in hot paths. Lambdas that capture several data members, a string, or a shared_ptr frequently exceed the SBO threshold, triggering a heap allocation on each set_strategy call. If this is on a hot path, prefer a pre-allocated strategy object stored externally and passed as a raw or non-owning reference.

std::function rejecting move-only callables. Attempting to store a lambda that captures unique_ptr, ofstream, or any other non-copyable in a std::function is a hard compile error. The fix is std::move_only_function (C++23) or restructuring ownership so the resource lives outside the lambda.

Template policy type explosion. Each Policy instantiation generates a separate class. A context class with a large body multiplied across many policies inflates binary size and build times. If policy count is large, measure whether a runtime approach is acceptable before committing to the template path.

Strategy when a branch would do. Adding an indirection layer to swap between two behaviours that are never actually swapped at runtime is unnecessary complexity. Strategy is appropriate when the variation is real β€” driven by configuration, user input, or runtime conditions β€” not when a compile-time if constexpr or a simple function argument suffices.

Strategy with std::function

std::function is the lowest-friction runtime strategy vehicle. It erases the concrete callable type behind a uniform signature, so the context never needs to know whether it holds a lambda, a free function, a member function via std::bind, or a hand-rolled functor.

cpp
// C++11
#include <functional>
#include <string>
#include <iostream>

class EmailDispatcher {
public:
    using DeliveryFn = std::function<bool(const std::string& to, const std::string& body)>;

    explicit EmailDispatcher(DeliveryFn fn) : deliver_{std::move(fn)} {}

    void set_strategy(DeliveryFn fn) { deliver_ = std::move(fn); }

    bool send(const std::string& to, const std::string& body) {
        return deliver_(to, body);
    }

private:
    DeliveryFn deliver_;
};

// SMTP strategy
EmailDispatcher prod{[](const std::string& to, const std::string& body) {
    std::cout << "[SMTP] -> " << to << ": " << body << '\n';
    return true;
}};

// Stub strategy for unit tests β€” no network, no side-effects
EmailDispatcher test{[](const std::string&, const std::string&) {
    return true;  // always succeeds, logs nothing
}};

The std::function copy requirement means the stored callable must itself be copyable. Capturing a unique_ptr or ofstream directly fails at compile time. Wrap the resource in a shared_ptr to regain copyability at the cost of a reference count, or switch to std::move_only_function (C++23).

cpp
// C++23 β€” move-only callable owning a non-copyable resource
#include <functional>
#include <memory>
#include <fstream>

std::move_only_function<void(std::string_view)> make_file_sink(const char* path) {
    auto fs = std::make_unique<std::ofstream>(path, std::ios::app);
    return [f = std::move(fs)](std::string_view msg) mutable {
        *f << msg << '\n';
    };
}

Policy-based design (compile-time strategy)

Policy-based design selects strategies at instantiation rather than at runtime, giving the compiler full view of each algorithm for inlining and constant-folding. Andrei Alexandrescu popularised the idiom in Modern C++ Design; the standard library itself uses it for allocator, comparator, and hash policies.

cpp
// C++11
#include <algorithm>
#include <vector>
#include <functional>

// Each policy is a stateless struct β€” costs nothing to store
struct AscendingPolicy {
    template<typename Iter>
    static void sort(Iter first, Iter last) {
        std::sort(first, last);
    }
};

struct DescendingPolicy {
    template<typename Iter>
    static void sort(Iter first, Iter last) {
        std::sort(first, last, std::greater<typename std::iterator_traits<Iter>::value_type>{});
    }
};

struct StableAscendingPolicy {
    template<typename Iter>
    static void sort(Iter first, Iter last) {
        std::stable_sort(first, last);
    }
};

// Context is parameterised β€” each Policy produces a distinct type
template<typename SortPolicy = AscendingPolicy>
class ReportGenerator {
    std::vector<int> rows_;
public:
    void add(int v) { rows_.push_back(v); }

    void finalise() {
        SortPolicy::sort(rows_.begin(), rows_.end());  // inlined, zero overhead
    }

    const std::vector<int>& rows() const { return rows_; }
};

ReportGenerator<AscendingPolicy>  asc;
ReportGenerator<DescendingPolicy> desc;

Multiple orthogonal policies can be composed via multiple template parameters, each controlling one independent axis of behaviour (e.g., sort order, allocation, serialisation format). The downside: each unique combination is a distinct type, so collections of mixed-policy objects require a further type-erasure layer.

cpp
// C++11 β€” composing two independent policies
template<typename SortPolicy = AscendingPolicy,
         typename AllocPolicy = std::allocator<int>>
class DataStore {
    std::vector<int, AllocPolicy> data_;
public:
    void sort() { SortPolicy::sort(data_.begin(), data_.end()); }
};

Lambda as strategy

Lambdas are the most concise way to express a one-off strategy inline. They avoid naming a class just to hold a single operation, and they capture surrounding state naturally without requiring custom constructor arguments.

cpp
// C++11
#include <vector>
#include <algorithm>
#include <string>

struct Record {
    std::string name;
    int priority;
    double score;
};

// Strategy injected as a lambda β€” no wrapper class needed
auto run_pipeline = [](std::vector<Record>& records,
                       auto sort_strategy,       // C++14 generic lambda
                       auto filter_strategy) {
    sort_strategy(records);
    records.erase(
        std::remove_if(records.begin(), records.end(), filter_strategy),
        records.end()
    );
};

std::vector<Record> data = { {"alpha", 2, 0.9}, {"beta", 1, 0.4}, {"gamma", 3, 0.7} };

// Sort by priority descending, drop low scores
run_pipeline(
    data,
    [](std::vector<Record>& r) {
        std::sort(r.begin(), r.end(),
            [](const Record& a, const Record& b){ return a.priority > b.priority; });
    },
    [](const Record& rec) { return rec.score < 0.5; }  // filter predicate
);

Stateful lambdas capture by value or reference. Capture by value is safer for strategies stored beyond the current scope; capture by reference is appropriate only when the lambda's lifetime is strictly nested inside the referred-to variables.

cpp
// C++17 β€” lambda capturing configuration from outer scope
double threshold = 0.6;
auto score_filter = [threshold](const Record& r) {   // captures by value β€” safe to store
    return r.score < threshold;
};

// threshold can now be modified; score_filter still uses the captured snapshot
threshold = 0.0;  // does NOT affect score_filter

Strategy with concepts (C++20)

Concepts turn implicit template requirements into compiler-enforced contracts. Without concepts, passing a type that lacks the expected interface produces cryptic substitution-failure errors deep inside the instantiation chain; with concepts the failure is immediate and the message names the violated constraint.

cpp
// C++20
#include <concepts>
#include <vector>
#include <ranges>

// Concept: anything that can sort a random-access range of ints
template<typename S>
concept IntSortStrategy = requires(S s, std::vector<int>& v) {
    { s.sort(v) } -> std::same_as<void>;
};

// Concept: anything that can compress/decompress byte buffers
template<typename C>
concept Codec = requires(C c,
                         std::vector<std::byte>& in,
                         std::vector<std::byte>& out) {
    { c.compress(in)   } -> std::same_as<std::vector<std::byte>>;
    { c.decompress(in) } -> std::same_as<std::vector<std::byte>>;
};

template<IntSortStrategy S>
class SortingPipeline {
    S strategy_;
public:
    explicit SortingPipeline(S s) : strategy_{std::move(s)} {}

    void run(std::vector<int>& v) { strategy_.sort(v); }
};

// Constraint violation produces a clear error at the call site
struct BrokenSort {
    int sort(std::vector<int>& v);  // wrong return type β€” int, not void
};

// SortingPipeline<BrokenSort> p;  // error: constraint not satisfied: IntSortStrategy<BrokenSort>

Concepts also enable if constexpr dispatch on strategy capabilities β€” a strategy can advertise optional features via nested type aliases or additional function signatures, and the context can adapt at compile time without runtime overhead.

cpp
// C++20 β€” optional capability detection via concepts
template<typename S>
concept StableIntSortStrategy = IntSortStrategy<S> && requires(S s) {
    { S::is_stable } -> std::convertible_to<bool>;
    requires S::is_stable == true;
};

template<IntSortStrategy S>
void sort_with_info(S& s, std::vector<int>& v) {
    if constexpr (StableIntSortStrategy<S>) {
        // compiler knows the sort is stable β€” can skip a verification step
    }
    s.sort(v);
}

Combining strategies with chain-of-responsibility

A strategy can delegate internally to a chain of sub-strategies, gaining composability without making the context aware of the chain's structure. The outer strategy acts as a uniform entry point while the chain decides which link handles the request.

cpp
// C++17
#include <functional>
#include <vector>
#include <string>
#include <optional>

// Each handler in the chain: returns a result or passes on
using Handler = std::function<std::optional<std::string>(const std::string&)>;

class ChainedValidationStrategy {
    std::vector<Handler> chain_;
public:
    void add_handler(Handler h) { chain_.push_back(std::move(h)); }

    // Strategy interface β€” the context only calls this
    std::optional<std::string> validate(const std::string& input) const {
        for (const auto& h : chain_) {
            auto result = h(input);
            if (result) return result;  // first match wins
        }
        return std::nullopt;  // all handlers passed
    }
};

ChainedValidationStrategy v;

v.add_handler([](const std::string& s) -> std::optional<std::string> {
    if (s.empty()) return "input must not be empty";
    return std::nullopt;
});

v.add_handler([](const std::string& s) -> std::optional<std::string> {
    if (s.size() > 255) return "input exceeds maximum length";
    return std::nullopt;
});

v.add_handler([](const std::string& s) -> std::optional<std::string> {
    for (char c : s)
        if (!std::isalnum(static_cast<unsigned char>(c)) && c != '_')
            return "input contains invalid characters";
    return std::nullopt;
});

// Context uses a single strategy interface, unaware of the chain internals
if (auto err = v.validate(user_input)) {
    report_error(*err);
}

This combination keeps the context simple (it calls one validate method) while making individual rules independently testable and reorderable at runtime. The chain itself is a strategy from the context's perspective β€” it can be swapped wholesale for a different validation regime.

Runtime vs compile-time strategy trade-offs

The choice between runtime and compile-time strategy is not an optimisation question alone β€” it is a question about when the variation is known. Runtime strategy is mandatory when the choice depends on user input, configuration files, or data arriving after the program starts. Compile-time strategy is appropriate when the variation is fixed per build target, library instance, or type instantiation.

FactorRuntime (virtual / std::function)Compile-time (template policy / concept)
Algorithm selectedAfter program startsBefore compilation finishes
Indirection costOne indirect call per invocationZero β€” fully inlined
Binary sizeOne copy of context classOne copy per policy combination
Type compatibilityAll strategies share a common typeEach policy combination is a distinct type
Error messagesRuntime (assertion / exception)Compile-time (concept violation)
TestabilityInject mock via set_strategyInstantiate with a test policy type
C++ standardC++98 (virtual), C++11 (std::function)C++98 (template), C++20 (concepts)

Prefer runtime strategy when the selection is driven by external state or the context must be stored in a heterogeneous collection. Prefer compile-time strategy when the selection is fixed per instantiation and call-site performance is critical (compression hot path, allocation policy, numeric kernel).

Mixed scenarios arise frequently: a top-level runtime strategy delegates to an inner compile-time policy. For example, a configurable codec chain selects the outer pipeline at runtime while each codec stage is a zero-overhead policy-based template β€” giving flexibility at the boundary and maximum performance in the kernel.

cpp
// C++20 β€” hybrid: runtime outer selection, compile-time inner policy
template<typename CompressionPolicy>
struct CompressStage {
    void process(std::vector<std::byte>& buf) {
        buf = CompressionPolicy::compress(buf);  // inlined
    }
};

class Pipeline {
public:
    using StageFn = std::function<void(std::vector<std::byte>&)>;
    void add_stage(StageFn s) { stages_.push_back(std::move(s)); }

    void run(std::vector<std::byte>& buf) {
        for (auto& stage : stages_) stage(buf);  // runtime dispatch at stage level
    }

private:
    std::vector<StageFn> stages_;
};

Pipeline p;
p.add_stage([](std::vector<std::byte>& b) {
    CompressStage<ZstdCodec>{}.process(b);   // compile-time policy, runtime boundary
});

See Also

  • Policy-Based Design β€” compile-time strategy via template parameters; generalises to multiple orthogonal policies composed through multiple inheritance
  • Type Erasure β€” the mechanism std::function uses internally; implement it directly when you need custom SBO sizes or non-copyable semantics before C++23
  • CRTP β€” static polymorphism alternative when virtual dispatch overhead is unacceptable and the type hierarchy is fixed