Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 67

Implementing Strategy Pattern with Polymorphism/Templates

Recommended resources

Hello! Welcome to our lesson on the Strategy pattern.

In our previous lesson, we explored the Decorator pattern, which showed us how to add new responsibilities to an object by wrapping it in layers. The key was adding functionality while maintaining the same interface.

Today, we'll dive into the Strategy pattern, a behavioral pattern that also deals with varying an object's behavior, but in a fundamentally different way. Instead of wrapping an object to add features (changing its "skin"), the Strategy pattern allows you to completely swap out an internal algorithm (changing its "guts"). This is incredibly powerful for decoupling an object from the specific way it performs a task.

Our learning outcome is to implement the Strategy pattern using polymorphism or templates to define a family of algorithms and make them interchangeable. Crucially, as you're interested in the "under the hood" aspects, we will dissect the performance and memory implications of each approach, contrasting the classic runtime polymorphic implementation with the modern compile-time template-based method.

1. The Problem: When One Algorithm Doesn't Fit All

Imagine you are developing different types of robots. Each robot needs to perform actions like search(), attack(), and defend(). You might initially use inheritance to define different robot types based on their primary behavior, for example, a LinearSearchRobot and a SpiralSearchRobot.

But what happens when you need to combine different attack and defend behaviors? You quickly run into the "class explosion" problem we saw with the Decorator pattern, where you need classes like LinearSearchPinchAttackRunDefendRobot, SpiralSearchPunchAttackHideDefendRobot, etc. The inheritance hierarchy becomes a rigid, unmaintainable web of combined behaviors.

C++ - strategy

To see this problem in action, let's watch the beginning of the video 'C++ - strategy' by Bradley Needham. He does an excellent job of demonstrating how a deep inheritance hierarchy becomes unwieldy when you need to mix and match algorithms.

Please watch from the beginning until 04:08. Focus on how adding new attack or defend algorithms forces the creation of many new classes and duplicates code.

This inflexibility highlights the need for a better design. We need to vary the algorithms independently from the Robot object that uses them.


2. The Solution: Encapsulating Algorithms

The Strategy pattern addresses this by defining a family of algorithms, encapsulating each one, and making them interchangeable. The pattern has two main participants:

  1. Context: The class that needs to perform an algorithm (e.g., our Robot class). It maintains a reference to a Strategy object.
  2. Strategy: An interface that defines the common signature for all supported algorithms.
  3. Concrete Strategy: A specific implementation of an algorithm (e.g., LinearSearch, PinchAttack).

The Context delegates the work to its Strategy object instead of implementing the behavior itself.

UML Class Diagram of the Strategy Design Pattern
This UML diagram shows the classic structure. The `Context` is configured with a concrete `Strategy` object (like `ConcreteStrategyA` or `B`) and simply calls `executeStrategy()`, delegating the actual work to the contained strategy object's `executeAlgorithm()` method.

This design allows the algorithm to be selected and swapped at runtime, independently from the client using the Context.


3. Implementation 1: Runtime Polymorphism

The classic implementation of the Strategy pattern uses runtime polymorphism via inheritance and virtual functions. Let's see how the Robot example is refactored using this approach.

C++ - strategy

The same video continues by implementing this classic, polymorphic version. This will show you the core mechanics of the pattern.

Watch from 04:08 to 09:20. Notice how the Robot class now holds pointers to SearchStrategy, AttackStrategy, and DefendStrategy base classes. The robot's methods simply delegate the call to these strategy objects.

The video's implementation uses raw pointers, which raises ownership questions. In modern C++, we'd manage the strategy objects' lifetimes with smart pointers. Here is a more robust implementation for a sorting context, using std::shared_ptr.

Policy based design pattern

The article 'Policy based design pattern' provides a clean, modern C++ example of the runtime polymorphic Strategy pattern. This will solidify your understanding of the classic approach.

Read the section 'Example of Strategy Design Pattern'. The code shows a Context class that can be configured with different SortStrategy implementations at runtime using std::shared_ptr.

Under the Hood: The Cost of Runtime Polymorphism

Your interest in low-level mechanics is key here. While flexible, this approach has performance implications you should be aware of:

  1. Vtable Overhead: The call strategy->sort(data) is a virtual function call. This involves:

    • Dereferencing the object's vptr to find the vtable.
    • Indexing into the vtable to get the function address.
    • Calling the function via that address (an indirect function call).
      This is more work than a direct function call and can hinder branch prediction.
  2. Optimization Barrier: Because the exact function being called is unknown at compile time, the compiler cannot perform optimizations like inlining. The virtual call acts as an "optimization barrier."

  3. Memory Management: Using std::shared_ptr is safe but introduces its own overhead. Every time a shared_ptr is copied (e.g., passed by value), its internal reference count must be atomically updated. These atomic operations can be surprisingly expensive, especially in multi-threaded contexts, as they require synchronization across CPU cores.

Rethink Polymorphism in C++ - Nicolai Josuttis - C++ on Sea 2025

Just how expensive can shared_ptr be? The talk 'Rethink Polymorphism in C++' by C++ expert Nicolai Josuttis includes a striking demonstration of this cost. This is a must-watch for anyone interested in C++ performance.

Watch the segment from 24:17 to 40:47. The speaker benchmarks a loop that copies std::shared_ptrs versus one that passes them by reference. Pay attention to the massive performance difference and the explanation involving atomic operations. This highlights a real-world cost of some dynamic polymorphism patterns.


4. Implementation 2: Compile-Time Polymorphism (Policy-Based Design)

What if you know which algorithm you need at compile time? C++ templates offer a powerful and highly efficient alternative. This variation of the Strategy pattern is commonly known as Policy-Based Design.

Instead of an inheritance hierarchy, you pass the strategy (now called a "policy") as a template parameter.

Policy based design pattern

The article 'Policy based design pattern' introduces this concept perfectly. It shows how to refactor the sorting example to use templates instead of virtual functions.

Please read the entire article, but focus on these key parts: 1) The 'Policy-Based Design in C++11' example to see the template implementation. 2) The 'Comparison' section that explicitly links Strategy to Policy-Based Design. 3) The 'Performance Comparison' section which summarizes the trade-offs.

Here is the core idea:

// A policy is just a class with a static method. No virtual functions!
struct AscendingSortPolicy {
    template <typename T>
    static void sort(std::vector<T>& data) {
        std::sort(data.begin(), data.end());
    }
};

// The Context is now a template, parameterized by the policy.
template <typename T, typename SortingPolicy>
class PolicyBasedContainer {
    // ...
    void sort() {
        // Direct, compile-time call to the policy's method.
        SortingPolicy::sort(data);
    }
    // ...
};

// Usage: The type itself defines the strategy.
PolicyBasedContainer<int, AscendingSortPolicy> myContainer;

Under the Hood: The Power of Static Polymorphism

This approach has significant advantages that align with your goal of understanding high-performance C++:

  1. No Runtime Overhead: The call SortingPolicy::sort(data) is resolved at compile time. There is no vptr, no vtable, and no indirect function call.
  2. Compiler Optimization: Since the compiler knows exactly which function is being called, it can inline the call, effectively replacing the function call with the function's body. This eliminates function call overhead entirely and opens the door for further optimizations across the function boundary.
  3. No Pointer Indirection: The context object doesn't need to store pointers to strategies, simplifying memory layout and management.

The primary trade-off is the loss of runtime flexibility. The algorithm is "baked in" to the type at compile time. You cannot change the sorting policy of a PolicyBasedContainer<int, AscendingSortPolicy> object at runtime.


5. Other C++ Alternatives

Modern C++ provides even more tools that can be used to implement strategy-like behavior:

  • std::function: A type-erased wrapper that can hold any callable entity (function pointers, lambdas, functors). This allows for runtime flexibility without a rigid inheritance hierarchy. It combines the flexibility of runtime polymorphism with the convenience of not needing a base class. However, it has its own overhead, including potential heap allocation for the wrapped callable. The final segment of the "C++ - strategy" video (LINK from 09:20) demonstrates this approach.

  • std::variant and std::visit: For a closed set of strategies, std::variant provides a type-safe union to hold one of the strategy types by value. This can be more cache-friendly than pointer-based approaches. std::visit provides a way to perform an operation on the currently active type. The "Rethink Polymorphism in C++" video (LINK, from 51:37) introduces this as a powerful alternative to traditional polymorphism.

Test your understanding!

You are building a data serialization library. You need to support multiple output formats, such as JSON and XML. You have a Serializer class that takes some data and writes it to a string.

  1. Sketch a runtime polymorphic design for this. Define a FormatStrategy interface and show how the Serializer (Context) would use it.
  2. Sketch a compile-time policy-based design. Define a JsonFormatPolicy and show how the Serializer template would use it.
  3. Describe a scenario where the runtime design is superior and one where the compile-time design is the clear winner.
Show answer

1. Runtime Polymorphic Design

#include <iostream>
#include <string>
#include <memory>

// Strategy Interface
class IFormatStrategy {
public:
    virtual ~IFormatStrategy() = default;
    virtual std::string format(const std::string& data) const = 0;
};

// Concrete Strategies
class JsonStrategy : public IFormatStrategy {
public:
    std::string format(const std::string& data) const override {
        return "{ \"data\": \"" + data + "\" }";
    }
};

class XmlStrategy : public IFormatStrategy {
public:
    std::string format(const std::string& data) const override {
        return "<data>" + data + "</data>";
    }
};

// Context
class Serializer {
private:
    std::unique_ptr<IFormatStrategy> strategy_;
public:
    // Strategy can be changed at runtime
    void setStrategy(std::unique_ptr<IFormatStrategy> strategy) {
        strategy_ = std::move(strategy);
    }

    std::string serialize(const std::string& data) {
        if (!strategy_) throw std::runtime_error("Strategy not set!");
        return strategy_->format(data);
    }
};

2. Compile-Time Policy-Based Design

#include <iostream>
#include <string>

// Policies (no common base class or virtual functions)
struct JsonFormatPolicy {
    static std::string format(const std::string& data) {
        return "{ \"data\": \"" + data + "\" }";
    }
};

struct XmlFormatPolicy {
    static std::string format(const std::string& data) {
        return "<data>" + data + "</data>";
    }
};

// Templated Context
template <typename FormatPolicy>
class Serializer {
public:
    std::string serialize(const std::string& data) {
        return FormatPolicy::format(data);
    }
};

3. Scenarios

  • Runtime Design is Superior: Imagine a web server application where the client can request the data format via an HTTP header (e.g., Accept: application/json or Accept: application/xml). The server needs to choose the serialization strategy dynamically based on the incoming request. The runtime design is perfect for this.

  • Compile-Time Design is Superior: Consider a high-performance logging system for an embedded device. The log format (e.g., a compact binary format or a simple text format) is determined when the firmware is built and never changes. Using the policy-based design would result in the fastest possible serialization code, as the compiler could inline the formatting logic directly into the logging call site, eliminating all dispatch overhead.


Conclusion

We've seen that the Strategy pattern is a fundamental tool for creating flexible and maintainable systems by decoupling an object from the algorithms it uses.

Key Takeaways:

  • Intent: To define a family of algorithms, encapsulate each one, and make them interchangeable.
  • Runtime Polymorphism (Classic Strategy):
    • Mechanism: Inheritance and virtual functions.
    • Pros: Highly flexible; strategies can be swapped at any time.
    • Cons: Incurs runtime overhead from virtual dispatch, can be an optimization barrier, and may involve performance costs from memory management (e.g., atomic operations in shared_ptr).
  • Compile-Time Polymorphism (Policy-Based Design):
    • Mechanism: Templates.
    • Pros: Extremely performant due to compile-time resolution, enabling inlining and other optimizations.
    • Cons: Inflexible at runtime; the strategy is fixed when the code is compiled.

Understanding the trade-offs between these two powerful C++ implementation techniques is crucial for writing code that is not only well-designed but also highly performant.

Next Lesson Preview:
In our next lesson, we will cover the Observer pattern. We'll shift our focus from structuring an object's internal behavior to managing communication between objects. The Observer pattern provides a clean way for an object (the "subject") to notify a list of other objects (the "observers") about state changes, without being tightly coupled to them. This is fundamental to building reactive and event-driven systems.