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

You can find the coding example here on compiler explorer.

 

Let's get started with type erasure. This term is in my mind since a while and so where questions about that topic. So well, like the term implies we're erasing types and don't care about which type we have or we deal with. The very simple form most know:

// a simple based and a derived type
class base {};
class derived : public base {};

// a free function which accepts a base 
void do_something(const base& b) {
    // ...
}

// ...
// anywhere in our code the call with a derived type
// which now gets "erased" becasue we're using base in 
// this function 
do_something(derived{});

Fair enough, but this doesn't really answer the question what type erasure is (and this isn't the answer). It's runtime polymorphism as we know it.

Let's add context and imagine a game where we can create differenct characters. We'll use a knight and skeleton. If we'd do it as classic runtime polymorphism we'd probably create a base class like character or something. But we don't do a base class this time, instead we do:

// two independent classes a knight and a skeleton
// they are pretty much the same here ... 
class knight {
public:
    explicit knight(std::size_t strength) 
    : m_strength(strength) {}

    std::size_t get_strength() const {
        return m_strength;
    }
    void who_am_i() const {
        std::cout << "i'm a knight\n";
    }
    // possible other public member functions ... 
    // void render(){}
    // void attack(){}
private:
    std::size_t m_strength;
};

class skeleton {
public:
    explicit skeleton(std::size_t strength) 
    : m_strength(strength) {}

    std::size_t get_strength() const {
        return m_strength;
    }
    void who_am_i() const {
        std::cout << "i'm a skeleton\n";
    }
private:
    std::size_t m_strength;
};

Note: there is no base class in between knight and skeleton and they don't know each other. We don't have a classice interface yet, but we'll need to define a sort of interface now. Therefore we need:

  • concept -> represents our interface
  • model -> holds the value
  • character -> this represents a character, in this case knight or skeleton
// this will represent our abstract character
class character 
{
public:
    // we have a templated constructor which accepts "any" character
    template<typename T> 
    character(T&& value) {
        m_value = std::make_unique<c_model<T>>(std::move(value));
    }
    
    // we call the concrete who_am_i member functions in here
    void who_am_i() const {
        m_value->who_am_i();
    }

    // we call the concrete get_strength member functions in here
    std::size_t get_strength() const {
        return m_value->get_strength();
    }

private:   
    // this represents the interface 
    // unfortunately i can't use lower case concept here
    // because concept is a build in keyword
    struct c_concept {
        virtual ~c_concept() {}
        virtual std::size_t get_strength() const = 0;
        virtual void who_am_i() const = 0;
    };

    // and a templated model for each possible character 
    // implements the concept and holds a value (or character)
    template<typename T>
    struct c_model : public c_concept 
    {
        c_model(T const& value) 
        : m_value(value) {};

        std::size_t get_strength() const override {
            return m_value.get_strength(); 
        }
        void who_am_i() const {
            m_value.who_am_i();
        }

        T m_value;
    };

private:
    // and ultimately the final value or the specific character
    // we hold in our character class 
    std::unique_ptr<c_concept> m_value;
};

And now we can create a character

// lets create a character a knight
cwt::character character{cwt::knight(10)};
character.who_am_i(); // i'm a knight

// i changed my mind i want to be a skeleton
character = cwt::skeleton(2);
character.who_am_i(); // i'm a skeleton

And this is seems to answer the question what is type erasure, it's a templated constructor.

You can find the coding example here on compiler explorer.

 

Conclusion

At first this seemed to bit like an effort or complicated to me. But it has a lot of advantages. On the user side, there are just plain classes, knight and skeleton, no base class, no dependency between them.

So from a user perspective this is now easy to use (std::any is actually implemented in that way). Because you only have to care about your concrete characters.

But note, this is a really light weight example to understand the idea behind type erasure. If you want to dive deeper and use type erasure in production, I really recommend to watch Klaus Iglbergers talk from CppCon 2022.

He goes more into detail and also explains how to enable tests. He covers a lot and it's really helpful to watch.

I hope that helped.

Best Thomas

Previous
Previous

[C++] Combine Type Erasure And Strategies

Next
Next

[C++] Genetic Algorithm