Template-based Strategy uses C++ templates to select algorithms at compile time. The algorithm type is fixed when the class is instantiated, so the compiler can generate type-specific code and eliminate virtual function overhead.
The approach
In template-based Strategy, the algorithm is a template parameter:
#include <vector>
#include <algorithm>
// Strategy interface (concept, not abstract class)
// Any type with operator() can be a strategy
template<typename SortAlgorithm>
class DataProcessor {
private:
SortAlgorithm sorter;
std::vector<int> data;
public:
DataProcessor() = default;
void add_data(int value) {
data.push_back(value);
}
void process() {
sorter.sort(data);
}
void print_data() const {
for (int value : data) {
std::cout << value << " ";
}
std::cout << "\n";
}
};
// Different sorting strategies
struct QuickSortStrategy {
void sort(std::vector<int>& data) const {
// Quick sort implementation
std::sort(data.begin(), data.end(),
[](int a, int b) { return a < b; });
std::cout << "Sorted with QuickSort\n";
}
};
struct MergeSortStrategy {
void sort(std::vector<int>& data) const {
// Merge sort implementation (using std::stable_sort)
std::stable_sort(data.begin(), data.end(),
[](int a, int b) { return a < b; });
std::cout << "Sorted with MergeSort\n";
}
};
struct HeapSortStrategy {
void sort(std::vector<int>& data) const {
// Heap sort implementation
std::make_heap(data.begin(), data.end());
std::sort_heap(data.begin(), data.end());
std::cout << "Sorted with HeapSort\n";
}
};
// Usage
int main() {
// Create processor with QuickSort at compile-time
DataProcessor<QuickSortStrategy> processor_quick;
processor_quick.add_data(30);
processor_quick.add_data(10);
processor_quick.add_data(20);
processor_quick.process(); // Uses QuickSort
processor_quick.print_data();
// Create processor with MergeSort (different type!)
DataProcessor<MergeSortStrategy> processor_merge;
processor_merge.add_data(30);
processor_merge.add_data(10);
processor_merge.add_data(20);
processor_merge.process(); // Uses MergeSort
processor_merge.print_data();
return 0;
}
Key characteristics
Compile-time selection
The algorithm is fixed when the class template is instantiated:
DataProcessor<QuickSortStrategy> proc1; // Type 1
DataProcessor<MergeSortStrategy> proc2; // Type 2
// proc1 and proc2 are completely different types!
No virtual functions needed
Because the type is known at compile time, the compiler can inline strategy calls:
// Compiler knows exactly which sort() to call
// No virtual function lookup, no pointer indirection
void process() {
sorter.sort(data); // Direct call, likely inlined
}
Zero-cost abstraction
The compiler generates specialized code for each template instantiation. With optimization enabled (-O2), the overhead falls to zero:
// With optimizations, this compiles down to direct algorithm calls
// No extra indirection or memory overhead
Type safety
Different algorithm instantiations are different types:
DataProcessor<QuickSortStrategy> quick_proc;
DataProcessor<MergeSortStrategy> merge_proc;
// This won't compile! Types are different.
// quick_proc = merge_proc; // ERROR
// But you can pass them to template functions:
template<typename T>
void do_work(DataProcessor<T>& processor) {
processor.process();
}
do_work(quick_proc); // OK
do_work(merge_proc); // OK
Strategy interface (concept)
In template-based Strategy, there is no explicit base class. Instead, strategies must satisfy a concept:
// Concept: SortingStrategy must have:
// - void sort(std::vector<int>&) const
// These satisfy the concept:
struct QuickSortStrategy {
void sort(std::vector<int>& data) const { /* ... */ }
};
struct MergeSortStrategy {
void sort(std::vector<int>& data) const { /* ... */ }
};
// This does NOT satisfy the concept (wrong method signature):
struct BadStrategy {
void sort_data(std::vector<int>& data) const { /* ... */ }
};
// Error: DataProcessor<BadStrategy> won't compile
Real-world example: string compression
struct GzipStrategy {
std::vector<uint8_t> compress(const std::string& data) const {
// Gzip compression
return /* compressed data */;
}
};
struct ZstdStrategy {
std::vector<uint8_t> compress(const std::string& data) const {
// Zstandard compression
return /* compressed data */;
}
};
struct NoCompressionStrategy {
std::vector<uint8_t> compress(const std::string& data) const {
// No compression, just copy
return std::vector<uint8_t>(data.begin(), data.end());
}
};
template<typename CompressionStrategy>
class FileCompressor {
private:
CompressionStrategy strategy;
public:
std::vector<uint8_t> compress_file(const std::string& filename) {
std::string content = read_file(filename);
return strategy.compress(content);
}
};
// Usage at compile-time
int main() {
FileCompressor<GzipStrategy> gzip_compressor;
auto compressed = gzip_compressor.compress_file("large_file.txt");
FileCompressor<NoCompressionStrategy> fast_compressor;
auto fast_copy = fast_compressor.compress_file("fast_file.txt");
return 0;
}
Advantages
- Zero runtime overhead. There are no virtual function calls, and the compiler can inline the algorithm.
- Type safety. The compiler catches mismatched strategy uses at compile time.
- Performance. The compiler can apply aggressive optimizations.
- Cache-friendly. Monomorphic (single-type) code paths improve cache locality.
- Small binary footprint. Each instantiation is specialized for its type.
- Deterministic behavior. All decisions are made before the program runs, so nothing depends on runtime type information.
Disadvantages
- The strategy must be known at compile time. You cannot change the algorithm after compilation.
- Code bloat. Each template instantiation generates new code (template code generation overhead).
- Binary size. Multiple instantiations can increase executable size.
- Compilation time. Template instantiation adds compilation overhead.
- No runtime flexibility. You cannot load strategies from configuration files or plugins.
- Less flexible. Different types are hard to store in the same container.
Comparison: different containers
// These types are DIFFERENT, can't be mixed:
std::vector<DataProcessor<QuickSortStrategy>> processors_quick;
std::vector<DataProcessor<MergeSortStrategy>> processors_merge;
// Can't do:
std::vector<DataProcessor<?>> mixed; // What type is "?"?
// If you need a homogeneous container, use std::variant (C++17):
using ProcessorVariant = std::variant<
DataProcessor<QuickSortStrategy>,
DataProcessor<MergeSortStrategy>,
DataProcessor<HeapSortStrategy>
>;
std::vector<ProcessorVariant> mixed_processors;
mixed_processors.push_back(DataProcessor<QuickSortStrategy>{});
mixed_processors.push_back(DataProcessor<MergeSortStrategy>{});
// Call process on each (requires std::visit):
for (auto& proc_variant : mixed_processors) {
std::visit([](auto& proc) { proc.process(); }, proc_variant);
}
When to use template-based strategy
- The algorithm must be known at compile time
- You need a zero-cost abstraction
- Algorithm instances stay type-distinct throughout the program
- You do not need container storage, or
std::variantis acceptable - Compile-time performance is acceptable (template instantiation overhead)
For cases where the algorithm needs to be selected at runtime, see Runtime Selection (Virtual Functions).
See also
- Runtime Selection (Virtual Functions). Virtual function approach for runtime selection.
- Policy Based Design. Advanced template technique using policies.
- Template Method. Similar name, different pattern. It defines an algorithm skeleton.
Summary
Template-based Strategy uses C++ templates for compile-time algorithm selection. The strategy type is a template parameter, so the compiler generates type-specialized code with no virtual function overhead. This approach gives up runtime flexibility for performance and type safety. Use it when the algorithm is fixed at compile time. If the algorithm comes from a config file, you will end up with a variant and a visitor, and the simple benefit is gone.