[C++] Combine Type Erasure And Strategies

You can find the coding example here on compiler explorer.

 

In this article we'll continue on type erasuer. In my previous article Use Type Erasure To Get Dynamic Polymorphic Behavior you can find the basic idea behind type erasure.

Let's recall what we had:

  • Two independent charakters, knight and skeleton
  • A character whith a templated constructer to hold knight or skeleton
  • Inside character we had c_concept and c_model which represents a character interface

Our character class was like this:

// this represents any character
class character 
{
public:
    // we had a templated constructor, to create concrete characters
    template<typename T> 
    character(T&& value);

    // two public member functions
    void who_am_i() const;
    std::size_t get_strength() const;

private:   
    // and our interface what functions a character needs to implement
    struct c_concept {
        virtual ~c_concept() {}
        virtual std::size_t get_strength() const = 0;
        virtual void who_am_i() const = 0;
    };

    template<typename T>
    struct c_model : public c_concept 
    {
        std::size_t get_strength() const override;
        void who_am_i() const override;

        T m_value;
    };

private:
    // finally, we'll hold in m_value the charcter
    std::unique_ptr<c_concept> m_value;
};

Now imagine you were given the task to add a render method to render a character to the screen.

It might appear easy to just extend c_concept (our interface) with render and call a render function on the concrete character (similar to what we did with who_am_i and get_strength). Then we'd leave the implementation to each concrete character. But here issues are arrising:

  • Imagine you already have several characters and moving the rendering to the concrete class, you need to change your concretre classes
  • A render method most likely brings another external dependency into your project (any graphics library)
  • If you already have unittests on your character, you'll also put a potential graphics library to your unittests
  • It increases complexity on the clients code, because we moved rendering to them

Let's Introduce A Strategy

We'll modify our code to add a render strategy to our character. This is kind of the Strategy Pattern. If you aren't familiar with the strategy pattern, you can for instance read on Refactoring Guru about it. In short: It allows you to modify behavior of an object.

Take a look on the modification in character and see the comments:

class character 
{
public:
    // we extended the template parameters by a Renderer parameter
    template<typename T, typename Renderer> 
    character(T&& character, Renderer&& renderer) {
        // the given Renderer is passed to our model
        m_character = std::make_unique<c_model<T, Renderer>>(std::move(character), std::move(renderer));
    }

    void who_am_i() const {
        m_character->who_am_i();
    }

    std::size_t get_strength() const {
        return m_character->get_strength();
    }
    
    // we add a render method to character
    void render()
    {
        m_character->render();
    }
private:   
    // we do extend our concept/interface to call render
    struct c_concept {
        virtual ~c_concept() {}
        virtual std::size_t get_strength() const = 0;
        virtual void who_am_i() const = 0;
        virtual void render() const = 0;
    };
    
    // we also extended the model with a Renderer type
    template<typename T, typename Renderer>
    struct c_model : public c_concept 
    {
        // the constructor receives the the renderer
        c_model(T const& value, Renderer const& renderer) 
        : m_character(value), m_renderer(renderer) {};

        std::size_t get_strength() const override {
            return m_character.get_strength(); 
        }
        void who_am_i() const override {
            m_character.who_am_i();
        }
        
        // we implement the render function in our model
        // note: we extended the members and instead of using our
        // concrete character, we'll use the Renderer which receives the character
        void render() const override{
            m_renderer(m_character);
        }

        T m_character;
        // and here we add the Renderer as member to our model
        Renderer m_renderer;
    };
private:
    std::unique_ptr<c_concept> m_character;
};

And these are all the changes we need to do in our character class, we don't need to change the concrete characters knight and skeleton. Now you can pass a render type/function to a character:

// this could be a renderer for a knight
// note: in the model we call the renderer just with the bracket operator
// therefore we simply overload it here
struct knight_renderer{
    void operator()(const cwt::knight& k) const {
        std::cout << "I'll take care of rendering" << std::endl;
    }
};

// we can also use a dedicated render function for knights
void knight_render_function(const cwt::knight& knight){
    std::cout << "I could also render a knight" << std::endl;
}

int main(){
    // lets create a character and pass a knight renderer to it
    cwt::character character{cwt::knight(10), knight_renderer{}};
    character.render();

    // lets create another knight, but this time 
    // we use the knight render function wrapped in a lambda
    character = cwt::character{cwt::knight(10), [](const cwt::knight& k){
        knight_render_function(k);
    }};
    character.render();

    // i changed my mind i want to be a skeleton, and let's render the 
    // skeleton inside the lambda
    character = cwt::character{cwt::skeleton(2), [](const cwt::skeleton& s){
        std::cout << "This might be another rendering technique" << std::endl;
    }};
    character.render();
}

You can find the entire example here on compiler explorer.

 

Conclusion

This is a very simple example, but it demonstrates the combination of a strategy and type erasuer. Just think about it, we haven't touched the concrete characters knight and skeleton and we can pass custom behavior from outside to it. If you'd have existing code you can modify the abstract character without forcing all concrete characters to change (of course you'd need to pass a default renderer or something similar to the constructor).

Also, if you want to test concrete characters or you already have tests for them, you can just pass a dummy to your concrete classes. If you take this example here, you wouldn't need to introduce a graphics library to your unittests because the rendering is encapsulated from the actual character.

I hope this could help and thats it for now.

Best Thomas

Previous
Previous

[C++] CRTP For Mixin Classes

Next
Next

[C++] Use Type Erasure To Get Dynamic Polymorphic Behavior