[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 interfacemodel
-> holds the valuecharacter
-> this represents a character, in this caseknight
orskeleton
// 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