Strategy Pattern
- Description: Define a family of interchangeable algorithms, encapsulate each, and let the client pick or swap one at runtime via composition — algorithm varies independently of the code that uses it.
- My Notion Note ID: K2C-2-20
- Created: 2026-05-22
- Updated: 2026-05-22
- License: Reuse is very welcome. Please credit Yu Zhang and link back to the original on yuzhang.io
Table of Contents
- 1. Core Idea
- 2. Structure
- 3. Classic C++ Example
- 4. Modern
std::functionForm - 5. When to Use / When Not To
- 6. Variants and Pitfalls
- 7. Related Patterns
- 8. References
1. Core Idea
- Replace
switch (algorithmKind)or hardcoded algorithm with a pluggable object. - Context delegates the algorithm step to a strategy object held via composition (pointer, reference,
std::function, template param). - Algorithm choice = data, not control flow.
- Canonical examples: comparator for sort, compression codec, validation rule, retry policy, route planner, payment processor.
2. Structure
- Context — holds a reference/handle to a
Strategy. Calls it at the variable step. - Strategy (interface) — declares the operation. Often a single method.
- ConcreteStrategyA, B, ... — implementations.
- Client — instantiates the concrete strategy and injects it into context (constructor or setter).
Composition over inheritance — context doesn't subclass per algorithm.
3. Classic C++ Example
#include <memory>
#include <vector>
#include <iostream>
#include <numeric>
struct Compressor {
virtual ~Compressor() = default;
virtual std::vector<std::byte> compress(const std::vector<std::byte>& in) = 0;
};
struct Zstd : Compressor { std::vector<std::byte> compress(const std::vector<std::byte>& in) override { /* zstd */ return in; } };
struct Gzip : Compressor { std::vector<std::byte> compress(const std::vector<std::byte>& in) override { /* gzip */ return in; } };
struct Noop : Compressor { std::vector<std::byte> compress(const std::vector<std::byte>& in) override { return in; } };
class Pipeline {
std::unique_ptr<Compressor> codec_;
public:
explicit Pipeline(std::unique_ptr<Compressor> c) : codec_(std::move(c)) {}
void setCodec(std::unique_ptr<Compressor> c) { codec_ = std::move(c); }
std::vector<std::byte> run(const std::vector<std::byte>& data) {
return codec_->compress(data);
}
};
int main() {
Pipeline p{std::make_unique<Zstd>()};
auto out = p.run({});
p.setCodec(std::make_unique<Gzip>());
}
4. Modern std::function Form
Many strategies are single-method → lift to std::function. No vtables, no class hierarchy:
#include <functional>
#include <vector>
#include <algorithm>
#include <string>
using Comparator = std::function<bool(const std::string&, const std::string&)>;
class Sorter {
Comparator cmp_;
public:
explicit Sorter(Comparator c) : cmp_(std::move(c)) {}
void sort(std::vector<std::string>& v) const { std::sort(v.begin(), v.end(), cmp_); }
};
int main() {
std::vector<std::string> v{"banana", "apple", "Cherry"};
Sorter byLex{[](auto& a, auto& b) { return a < b; }};
byLex.sort(v);
Sorter byLen{[](auto& a, auto& b) { return a.size() < b.size(); }};
byLen.sort(v);
Sorter caseInsensitive{[](auto& a, auto& b) {
return std::lexicographical_compare(a.begin(), a.end(), b.begin(), b.end(),
[](char x, char y) { return std::tolower(x) < std::tolower(y); });
}};
caseInsensitive.sort(v);
}
Compile-time variant — template parameter; algorithm picked at instantiation:
template <typename Cmp>
class StaticSorter {
Cmp cmp_;
public:
explicit StaticSorter(Cmp c = {}) : cmp_(std::move(c)) {}
void sort(std::vector<std::string>& v) const { std::sort(v.begin(), v.end(), cmp_); }
};
StaticSorter<std::less<>> s1;
StaticSorter<std::greater<>> s2;
This is the form std::sort, std::priority_queue, std::set already use — comparator + allocator as template parameters.
5. When to Use / When Not To
Use when:
- Multiple algorithms exist for the same task; choice varies per call site or per config.
- You want to add new algorithms without modifying the context (OCP).
- Algorithm needs to be configurable, mockable, hot-swappable.
Avoid when:
- Only one algorithm exists today and the change is purely speculative — premature abstraction.
- Algorithm is trivial (1–2 lines) and rarely changes — inline it.
- Differences between algorithms are deeper than a single hook (different inputs/outputs) — Strategy fits poorly.
6. Variants and Pitfalls
std::functionallocates for non-trivial captures (small buffer optimization is implementation-defined). Hot paths: prefer template parameter orfunction_ref(proposed) for non-owning.- Null strategy — always construct with a default (e.g.
Noopcodec) rather than nullable handle; spares every call site a null check. - Strategy explosion — many tiny strategies. Group via factory; expose enum/string identifier mapping to concrete strategy.
- Stateful strategies — usually a smell; strategies should be referentially transparent or scoped per call. If state grows, the strategy is becoming its own object.
- Lifetime — context shouldn't outlive its injected strategy. Pass
unique_ptror by value; raw pointers without ownership story bite later.
7. Related Patterns
- Strategy vs State — identical UML, opposite intent. Strategy: client chooses; algorithm doesn't change itself. State: the state object swaps itself based on internal transitions. Strategy = "how", State = "what mode am I in".
- Strategy vs Template Method — both define a varying step. Strategy uses composition + runtime swap. Template Method uses inheritance + compile-time hook in a subclass override. Choose Strategy when the variation needs to switch per call or be configured externally; Template Method when the variation is fixed per subclass.
- Policy-Based Design — compile-time Strategy via template parameters. See Policy Based Design. Zero overhead, but algorithm baked in at instantiation.
- CRTP — another compile-time form (CRTP) when policy classes derive from a common base.
- Command — strategy + invocation metadata (parameters, undo). Strategy is "how"; Command is "what to do + when".
- Decorator — adds behavior; Strategy replaces behavior.
8. References
- Gamma, Helm, Johnson, Vlissides. Design Patterns. Strategy chapter.
- cppreference: std::function
- cppreference: std::sort — Comparator as Strategy.
- Alexandrescu, Modern C++ Design — Ch 1 (Policy-Based Class Design) for the compile-time variant.