The Strategy Pattern in C++: Virtual Classes vs Function Pointers vs Lambdas vs std::function
Key takeaways
Strategy pattern in C++: polymorphic strategies, function pointers, lambdas, std::function—sorting and compression examples, plus performance trade-offs.
Strategy is one of the behavioral patterns, alongside State, Command and Observer. In C++ it is unusual among the classic patterns because the language gives you at least four ways to express it, each with a different cost model: a virtual interface, a plain function pointer, a lambda passed as a template parameter, and a type-erased std::function. This article implements the same idea all four ways and explains when each one is the right tool.
What is Strategy Pattern? why you need it
Problem Scenario: Hardcoding the Algorithm
Problem: If the logic for selecting a sorting algorithm is hardcoded in the Context, Context must be modified when adding a new algorithm.
// Bad example: hardcoding the algorithm
class Sorter {
public:
void sort(std::vector<int>& data, const std::string& algorithm) {
if (algorithm == "bubble") {
// bubble sort
} else if (algorithm == "quick") {
// quick sort
} else if (algorithm == "merge") {
// merge sort
}
// Edit here when adding a new algorithm
}
};
The problem with this version is not the if chain itself; three branches are perfectly readable. It is that every new algorithm requires editing and recompiling Sorter, every caller now depends on all three algorithms, the string "quick" is a typo waiting to happen (a misspelled name silently does nothing), and you cannot test one algorithm without going through the dispatcher. When the set of algorithms is small and fixed, that is an acceptable trade. When the set grows, when users or configuration choose the algorithm, or when different teams own different algorithms, it becomes a maintenance problem.
Solution: Strategy Pattern encapsulates the algorithm so that it can be replaced at runtime.
// Good example: Strategy Pattern
class SortStrategy {
public:
virtual void sort(std::vector<int>& data) = 0;
virtual ~SortStrategy() = default;
};
class BubbleSort : public SortStrategy {
void sort(std::vector<int>& data) override { /* ... */ }
};
class Sorter {
public:
void setStrategy(std::unique_ptr<SortStrategy> s) {
strategy = std::move(s);
}
void sort(std::vector<int>& data) {
strategy->sort(data);
}
private:
std::unique_ptr<SortStrategy> strategy;
};
flowchart TD
context["Context (Sorter)"]
strategy["Strategy (SortStrategy)"]
bubble[BubbleSort]
quick[QuickSort]
merge[MergeSort]
context -->|holds| strategy
bubble -->|implements| strategy
quick -->|implements| strategy
merge -->|implements| strategy
The pattern has three roles. The Strategy is the interface that describes what an algorithm does (sort(data)), the concrete strategies are the interchangeable implementations, and the Context (Sorter) holds one strategy and delegates to it. The Context depends only on the interface, so adding MergeSort means writing one new class and changing nothing that already exists, which is what the Open/Closed Principle asks for. Ownership through std::unique_ptr makes it explicit that the Sorter owns its strategy and destroys it when it is replaced or when the Sorter dies.
index
- Basic structure (polymorphic)
- Function pointer method
- Lambda method
- std::function method
- Frequently occurring problems and solutions
- Production Patterns
- Complete example: Compression algorithm
- Performance comparison
Basic structure (polymorphism)
Minimum Strategy
#include <iostream>
#include <vector>
#include <memory>
#include <algorithm>
class SortStrategy {
public:
virtual void sort(std::vector<int>& data) = 0;
virtual std::string name() const = 0;
virtual ~SortStrategy() = default;
};
class BubbleSort : public SortStrategy {
public:
void sort(std::vector<int>& data) override {
for (size_t i = 0; i < data.size(); ++i) {
for (size_t j = 0; j < data.size() - i - 1; ++j) {
if (data[j] > data[j + 1]) {
std::swap(data[j], data[j + 1]);
}
}
}
}
std::string name() const override { return "BubbleSort"; }
};
class QuickSort : public SortStrategy {
public:
void sort(std::vector<int>& data) override {
std::sort(data.begin(), data.end());
}
std::string name() const override { return "QuickSort"; }
};
class Sorter {
public:
void setStrategy(std::unique_ptr<SortStrategy> s) {
strategy = std::move(s);
}
void sort(std::vector<int>& data) {
if (strategy) {
std::cout << "Using " << strategy->name() << '\n';
strategy->sort(data);
}
}
private:
std::unique_ptr<SortStrategy> strategy;
};
int main() {
Sorter sorter;
std::vector<int> data = {5, 2, 8, 1, 9};
sorter.setStrategy(std::make_unique<BubbleSort>());
sorter.sort(data); // Using BubbleSort
sorter.setStrategy(std::make_unique<QuickSort>());
sorter.sort(data); // Using QuickSort
}
Two small details carry a lot of weight here. The virtual destructor in SortStrategy is required, because the unique_ptr<SortStrategy> deletes a BubbleSort through a base pointer; without it, the derived destructor would not run, which is undefined behavior. And setStrategy takes the unique_ptr by value and moves it into the member, so the caller’s intent (hand over ownership) is visible at the call site as std::make_unique<...>() or std::move(p).
The class named QuickSort delegates to std::sort, which in practice is introsort: quicksort that falls back to heapsort when recursion gets too deep and to insertion sort for small ranges. That is exactly why it is a good default and bubble sort is only here for contrast. The virtual call itself costs an indirect branch, a few nanoseconds; it is irrelevant when each call sorts thousands of elements, and it only starts to matter when the strategy is called millions of times per second on tiny inputs, in which case the compiler’s inability to inline through the vtable is the real cost.
function pointer method
Simple algorithm
#include <iostream>
#include <vector>
#include <algorithm>
using SortFunc = void(*)(std::vector<int>&);
void bubbleSort(std::vector<int>& data) {
for (size_t i = 0; i < data.size(); ++i) {
for (size_t j = 0; j < data.size() - i - 1; ++j) {
if (data[j] > data[j + 1]) {
std::swap(data[j], data[j + 1]);
}
}
}
}
void quickSort(std::vector<int>& data) {
std::sort(data.begin(), data.end());
}
class Sorter {
public:
void setStrategy(SortFunc func) {
strategy = func;
}
void sort(std::vector<int>& data) {
if (strategy) {
strategy(data);
}
}
private:
SortFunc strategy = nullptr;
};
int main() {
Sorter sorter;
std::vector<int> data = {5, 2, 8, 1, 9};
sorter.setStrategy(bubbleSort);
sorter.sort(data);
sorter.setStrategy(quickSort);
sorter.sort(data);
}
A function pointer is the smallest possible strategy: one machine word, trivially copyable, no allocation, and compatible with C APIs such as qsort’s comparator. The using SortFunc = void(*)(std::vector<int>&); alias makes the signature readable, and the compiler checks every assignment against it, so passing a function with the wrong parameters is a compile error, not a run-time surprise.
The limitation is state. A function pointer can only point at a free function or a captureless lambda, so there is nowhere to put configuration such as “sort descending” or “stop after N elements” other than global variables. A lambda that captures anything does not convert to a function pointer, and trying gives an error along the lines of no suitable conversion function from "lambda []void (std::vector<int> &)->void" to "SortFunc" exists. Calls through a function pointer are also indirect, so like virtual calls they usually cannot be inlined unless the optimizer can see which function the pointer holds.
lambda method
Inline Algorithm
#include <iostream>
#include <vector>
#include <functional>
#include <algorithm>
class Sorter {
public:
using Strategy = std::function<void(std::vector<int>&)>;
void setStrategy(Strategy s) {
strategy = s;
}
void sort(std::vector<int>& data) {
if (strategy) {
strategy(data);
}
}
private:
Strategy strategy;
};
int main() {
Sorter sorter;
std::vector<int> data = {5, 2, 8, 1, 9};
// Define Strategy with lambda
sorter.setStrategy([](std::vector<int>& data) {
std::sort(data.begin(), data.end());
});
sorter.sort(data);
// Sort in reverse order
sorter.setStrategy([](std::vector<int>& data) {
std::sort(data.begin(), data.end(), std::greater<>());
});
sorter.sort(data);
}
The lambdas themselves are what make this style pleasant: the strategy is written right where it is chosen, with no class declaration, and it can capture local state ([threshold](std::vector<int>& d) { ... }). Note, though, that this Sorter stores its strategy in a std::function, so the lambda is type-erased once it is stored. The lambda’s own type is unique and unnamed, which is exactly why you need either std::function or a template to store one.
The template alternative keeps the lambda’s concrete type and lets the compiler inline it:
template <typename SortFn>
void sortWith(std::vector<int>& data, SortFn&& fn) { fn(data); }
sortWith(data, [](std::vector<int>& d) { std::sort(d.begin(), d.end()); });
This is how the standard algorithms accept comparators, and it has zero dispatch overhead. The trade-off is that the strategy is fixed at compile time for each instantiation, you cannot store different lambdas in the same member variable or container, and each distinct lambda produces its own instantiation, which increases code size if you use many of them.
std::function method
Flexible Strategy
#include <iostream>
#include <vector>
#include <functional>
#include <algorithm>
class PaymentStrategy {
public:
using Strategy = std::function<bool(double)>;
void setStrategy(Strategy s) {
strategy = s;
}
bool pay(double amount) {
if (strategy) {
return strategy(amount);
}
return false;
}
private:
Strategy strategy;
};
int main() {
PaymentStrategy payment;
// credit card
payment.setStrategy([](double amount) {
std::cout << "Paying $" << amount << " with Credit Card\n";
return true;
});
payment.pay(100.0);
// PayPal
payment.setStrategy([](double amount) {
std::cout << "Paying $" << amount << " with PayPal\n";
return true;
});
payment.pay(50.0);
}
std::function<bool(double)> accepts anything callable with that signature: free functions, lambdas with or without captures, functor objects, and (via std::bind or a wrapping lambda) member functions. That flexibility is why it is the most common choice for callbacks and plug-in style strategies. The cost is type erasure. Calling through a std::function is an indirect call that the compiler generally cannot inline, and storing a callable whose captures do not fit the implementation’s small internal buffer (typically around two or three pointers’ worth) requires a heap allocation. A capture-free lambda like the ones above does not allocate in mainstream implementations; a lambda that captures a std::string by value usually does.
Two practical pitfalls come up often. Calling an empty std::function throws std::bad_function_call, which is why pay checks if (strategy) first. And std::function requires the stored callable to be copyable, so a lambda that captures a std::unique_ptr will not compile into it, with an error pointing deep into the <functional> header about a deleted copy constructor. C++23’s std::move_only_function removes that restriction. For a real payment flow, returning bool also throws away the reason a payment failed; a result type or exception that carries the error is usually worth the extra code.
Frequently occurring problems and solutions
Problem 1: Strategy nullptr
Symptom: Crash. Cause: Strategy is not set.
// ❌ Misuse: No nullptr check
void sort(std::vector<int>& data) {
strategy->sort(data); // Crash: nullptr
}
// ✅ Correct usage: nullptr check
void sort(std::vector<int>& data) {
if (strategy) {
strategy->sort(data);
} else {
throw std::runtime_error("Strategy not set");
}
}
A null strategy is really a design question: is “no strategy” a valid state? If it is not, the cleanest fix is to make it unrepresentable, by requiring a strategy in the constructor and rejecting nullptr in setStrategy (Pattern 1 below). If it is valid, the Null Object pattern is often nicer than a check at every call site: a NoOpSort strategy that does nothing keeps the Context free of if (strategy) branches. The throwing version shown here is the right choice when a missing strategy indicates a configuration error that should fail loudly. Dereferencing a null unique_ptr is undefined behavior, which on most platforms shows up as a segmentation fault at the call, but the optimizer is allowed to assume it never happens, so do not rely on the crash.
Problem 2: State sharing
Symptom: Behavior different from expected. Cause: If Strategy has state, it becomes problematic when reused.
// ❌ Misuse: state sharing
class CountingSort : public SortStrategy {
int count = 0; // situation
public:
void sort(std::vector<int>& data) override {
++count; // Accumulation when reused
}
};
// ✅ Correct use: Stateless Strategy
class CountingSort : public SortStrategy {
public:
void sort(std::vector<int>& data) override {
// no state, pure algorithm
}
};
State in a strategy is not wrong in itself; configuration such as a comparison order or a compression level is exactly what strategy objects are for. The problem is mutable state that changes as a side effect of calling the strategy, because it makes the result depend on call history. The bug usually appears when one strategy instance is shared: two Contexts hold the same shared_ptr<Strategy>, or a strategy is a static singleton, and one caller’s counter or cache leaks into the other’s results. If the strategy is shared across threads, that mutable member is also a data race.
The rules I follow are simple: configuration goes into constructor parameters and is const afterwards, per-call scratch data lives in local variables inside the method, and anything that genuinely must accumulate (metrics, caches) is either owned by the Context or explicitly synchronized. Marking the interface method const (virtual void sort(std::vector<int>&) const = 0;) lets the compiler enforce the first two.
production pattern
Pattern 1: Basic Strategy
class Sorter {
public:
Sorter() : strategy(std::make_unique<QuickSort>()) {} // default value
void setStrategy(std::unique_ptr<SortStrategy> s) {
if (s) {
strategy = std::move(s);
}
}
void sort(std::vector<int>& data) {
strategy->sort(data); // always valid
}
private:
std::unique_ptr<SortStrategy> strategy;
};
Pattern 2: Strategy Factory
class StrategyFactory {
public:
static std::unique_ptr<SortStrategy> create(const std::string& type) {
if (type == "bubble") return std::make_unique<BubbleSort>();
if (type == "quick") return std::make_unique<QuickSort>();
return nullptr;
}
};
int main() {
Sorter sorter;
sorter.setStrategy(StrategyFactory::create("quick"));
}
These two patterns work as a pair. The default strategy in the constructor means a freshly created Sorter is always usable, and setStrategy ignoring nullptr preserves that invariant, so sort can call through without a check. The factory moves the string-to-type mapping, the only place where names like "quick" appear, into one function, which is typically fed by a config file or command-line flag.
Notice how they interact: an unknown name makes the factory return nullptr, and setStrategy silently keeps the old strategy. That is convenient but can hide a typo in a config file for a long time. In production I prefer the factory to report unknown names loudly, either by throwing or by returning std::optional / an error the caller must handle, and to log which strategy was actually selected at startup. A registry (std::unordered_map<std::string, std::function<std::unique_ptr<SortStrategy>()>>) is the usual next step when strategies are added by plug-ins and the factory should not need editing for each new one.
Complete Example: Compression Algorithm
#include <iostream>
#include <string>
#include <memory>
#include <vector>
#include <cstdint>
#include <stdexcept>
class CompressionStrategy {
public:
virtual std::vector<uint8_t> compress(const std::string& data) = 0;
virtual std::string decompress(const std::vector<uint8_t>& data) = 0;
virtual std::string name() const = 0;
virtual ~CompressionStrategy() = default;
};
class ZipCompression : public CompressionStrategy {
public:
std::vector<uint8_t> compress(const std::string& data) override {
std::cout << "[ZIP] Compressing " << data.size() << " bytes\n";
std::vector<uint8_t> result(data.begin(), data.end());
return result;
}
std::string decompress(const std::vector<uint8_t>& data) override {
std::cout << "[ZIP] Decompressing " << data.size() << " bytes\n";
return std::string(data.begin(), data.end());
}
std::string name() const override { return "ZIP"; }
};
class GzipCompression : public CompressionStrategy {
public:
std::vector<uint8_t> compress(const std::string& data) override {
std::cout << "[GZIP] Compressing " << data.size() << " bytes\n";
std::vector<uint8_t> result(data.begin(), data.end());
return result;
}
std::string decompress(const std::vector<uint8_t>& data) override {
std::cout << "[GZIP] Decompressing " << data.size() << " bytes\n";
return std::string(data.begin(), data.end());
}
std::string name() const override { return "GZIP"; }
};
class Compressor {
public:
void setStrategy(std::unique_ptr<CompressionStrategy> s) {
strategy = std::move(s);
}
std::vector<uint8_t> compress(const std::string& data) {
if (!strategy) {
throw std::runtime_error("Compression strategy not set");
}
std::cout << "Using " << strategy->name() << " compression\n";
return strategy->compress(data);
}
std::string decompress(const std::vector<uint8_t>& data) {
if (!strategy) {
throw std::runtime_error("Compression strategy not set");
}
return strategy->decompress(data);
}
private:
std::unique_ptr<CompressionStrategy> strategy;
};
int main() {
Compressor compressor;
std::string data = "Hello, World! This is a test.";
compressor.setStrategy(std::make_unique<ZipCompression>());
auto compressed = compressor.compress(data);
auto decompressed = compressor.decompress(compressed);
std::cout << "Result: " << decompressed << "\n\n";
compressor.setStrategy(std::make_unique<GzipCompression>());
compressed = compressor.compress(data);
decompressed = compressor.decompress(compressed);
std::cout << "Result: " << decompressed << '\n';
}
The two “compression” classes are placeholders that copy bytes unchanged; in a real program they would wrap zlib, zstd or a similar library. What the example does show is the shape of a strategy with more than one operation. Compress and decompress must be a matched pair, so they belong in the same interface: splitting them into two independent strategies would let someone compress with ZIP and decompress with GZIP. <cstdint> and <stdexcept> were missing from the original listing; some standard library implementations pull them in transitively, but relying on that makes code break when you switch compilers.
The example also exposes the weak spot of switching strategies at run time. compressor.decompress(compressed) uses whatever strategy is currently set, not the one that produced the data. If the strategy changes between the two calls, or the compressed bytes were written to disk yesterday by a different configuration, decompression silently uses the wrong algorithm. Real formats solve this by writing an identifier into the output (gzip and zstd both start with magic bytes), and the reading side picks the strategy from that header rather than from the current setting. When a strategy’s output outlives the call, record which strategy produced it.
Performance comparison
| method | Advantages | Disadvantages |
|---|---|---|
| polymorphism | Type safe, extensible, can hold state and several related methods | Indirect call (usually not inlined), typically one heap allocation per strategy object |
| Function pointer | One word, no allocation, C-compatible | Cannot carry state; indirect call |
| Lambda (template parameter) | Inlined, zero dispatch cost, can capture | Strategy fixed at compile time; cannot store different lambdas in one variable |
| std::function | Accepts any callable, can be swapped at run time | Indirect call; heap allocation when captures exceed the small buffer; callable must be copyable |
Read the table as “what does it cost to call” and “what can change at run time”. Only the template-parameter lambda is truly free to call, and it is the only one that cannot be swapped at run time. The other three pay roughly the same price, one indirect call, and differ mainly in what they can hold. For a strategy that does real work per call (sorting a vector, compressing a buffer, charging a card), that call overhead is lost in the noise, and the choice should be driven by design: state, number of operations, and who defines new strategies. Measure before optimizing for dispatch cost; in my experience the call mechanism is almost never the bottleneck, while an unexpected std::function allocation inside a hot loop occasionally is.
organize
| concept | Description |
|---|---|
| Strategy Pattern | Runtime replacement by encapsulating algorithms |
| Purpose | Algorithm independence, scalability |
| Structure | Context, Strategy, ConcreteStrategy |
| Advantages | OCP compliance, removal of conditional statements, easy testing |
| Disadvantages | class increment, indirect reference |
| Use Case | Sorting, Compression, Payment, Routing |
The Strategy Pattern is a powerful design pattern for situations where algorithms need to be replaced dynamically.
FAQ
Q1: When do I use Strategy Pattern?
A: Used when you need to choose between multiple algorithms and replace them at runtime.
Q2: Polymorphism vs Lambda?
A: If scalability is important, use polymorphism, and if simple algorithm, use lambda.
Q3: What is the difference from State Pattern?
A: Strategy focuses on algorithm replacement, State focuses on state transition.
Q4: What is the performance overhead?
A: Virtual calls, function pointers and std::function all cost about one indirect call and usually prevent inlining; std::function may also allocate when it stores a callable with large captures. A lambda passed as a template parameter is the fastest because it can be inlined. For strategies that do meaningful work per call, the difference is rarely measurable.
Q5: How do I set the basic Strategy?
A: Set the default Strategy in the constructor.
Q6: What are Strategy Pattern learning resources?
A:
- “Design Patterns” by Gang of Four
- “Head First Design Patterns” by Freeman & Freeman
- Refactoring Guru: Strategy Pattern One line summary: Strategy Pattern allows you to encapsulate algorithms and replace them at runtime. Next, it would be a good idea to read Command Pattern.
Related Articles
- C++ virtual functions
- C++ Observer pattern
- C++ function pointers
- C++ lambda expressions
- CRTP vs Virtual Functions: Static Polymorphism Without vtable Overhead
- Factories in C++
- C++ Visitor Pattern
- Arrays and lists | Complete summary of essential data structures for coding tests
- The Adapter Pattern in C++
- The State Pattern in C++
- The Decorator Pattern in C++
- C++ function objects