Skip links

  • Skip to primary navigation
  • Skip to content
  • Skip to footer
Menu
  • Home
  • Posts
  • Tags
  • Categories
  • Projects
  • DogEars
  • About
deificle

My rant on programming and stuff

Type Erasure

Recently I came across a programming pattern that has been there for a while called Type Erasure Design Pattern. It has been very useful if abstracting types across multiple layers. I found it very useful when designing a graphics engine that we are creating at work.

What is Type Erasure?

As the name indicates, it erases or hides the type information completely from one part of the system to the other. The idea is quite easy to grasp if we have a basic understanding of [virtual functions]. For ease of describing what it is, I will build it on the examples used in the previous chapters, particularly from this [post] which is here again.

class Actor {
    virtual void render() = 0;
    virtual void update() = 0;
};

class Player : public Actor {
    virtual void render() override {
        printf("Player render\n");
        // render player stuff
    }

    virtual void update() override {
        printf("Player update\n");
        // update player stuff
    }

    // local/unique members and methods
};

class Monster : public Actor {
    virtual void render() override {
        printf("Monster render\n");
        // render monster stuff
    }

    virtual void update() override {
        printf("Monster update\n");
        // update monster stuff
    }
    
    // local/unique members and methods
};

By the end of the tutorial, we expanded the idea over various types. Here, all the types implicitly or explicitly are derived from the same type called Actor. In a large-scale system, there is no guarantee that this trait can be maintained. Further, if it should work, we should maintain a trait and make sure it is implicitly or explicitly exposed to various components and across all the components in the system. This can go out of control in no time. Whenever there is a new component that should be exposed to different types, it will be a PITA to design.

The Type Erasure Design Pattern is designed particularly to solve this messy design issue that “may” arise when using inheritance and polymorphism extensively. Let’s look at the solution in a step-by-step approach.

Functional Programming and Templates

The most widely used system programming language C++ encourages us to go full Object Oriented but doesn’t particularly talk much about the benefits of functional programming. Most importantly, C++ doesn’t prevent us from doing it either.

As discussed earlier, if the concrete types we are working on are guaranteed to be derived from the same base class, Inheritance and Polymorphism are good solutions. If, for some reason, we are unable to derive them from the same base type, then the inheritance and polymorphism may become messy over time. Thus, we can make use of the functional programming style to overcome the issue at hand and our solution will look something like this.

template <typename T>
void render(T* actor){
    actor->render();
}

template <typename T>
void update(T* actor){
    actor->update();
}

class Player {
    void render() {
        printf("Player render\n");
        // render player stuff
    }

    void update() {
        printf("Player update\n");
        // update player stuff
    }
};

class Monster {
    void render() {
        printf("Monster render\n");
        // render monster stuff
    }

    void update() {
        printf("Monster update\n");
        // update monster stuff
    }
};

This function can be called anywhere with any parameters, as long as the parameters passed defines the functions update and render. As you observe, there is no need for a class called Actor. The need for Monster and Player to share the same common base type is also unnecessary, thus the need for the keywords virtual and override is also gone(only for now).

These methods update and render works on Monster and Player because of the keyword template which plays a key role at compile time. Whenever a type is invoked on a templated function, the compiler generates an overloaded function of the templated function for that invocation. To make it clear, if we make sure that the program will compile if, in the templated method, the template type T is replaced by Monster or Player, the template invocation is valid.

Suppose, if there is a call like this somewhere in the program,

Player p;
render(&p);

The compiler will compile the function render(T* actor) by kinda replacing the T with Player. In layman’s terms, it may look something like this.

void render<Player>(Player *actor){
    actor->render();
}

Drawbacks of templated functions

There are no drawbacks in the pattern itself. Using render(T* actor) and update(T* actor) gives us an advantage by letting us get rid of sharing the same base class and introduces a more pressing problem. We lost the ability to shove it all under the same type as we did before! For instance, we can no longer do this.

Actor* actors [] = {
        new Player(),
        new Monster(),
        new Cyclops(),
        new Monster()
    };

Now, there is no type called Actor and there is no way to store anything without knowing its type.

Another drawback is, the caller should be aware of all the concrete types to begin with. This is more of an impossible-to-solve problem if you think about it. There is no way to expose all the concrete types to any system that may or may not use it and it is much more difficult when new types are introduced along the way.

If this should work without exposing the types, the caller must also be templated. This too, will quickly get out of hand and make it difficult to organize the program.

Wrapping it up

To fix this problem, let’s build a new interface! Here we are working on ways to implement render and update functionalities. We are looking for ways to implement the Actor type. Thus we reintroduce the same.

class Actor {
    public:
    virtual void render() = 0;
    virtual void update() = 0;
};

For the Player and Monster to work along with this Actor, we have to wrap them over a wrapper class like this.

class PActorWrapper : public Actor {
    private:
    Player *m_player;
    
    public:
    PActorWrapper(Player* player)
    :m_player(player){}

    virtual void render() { m_player->render(); }
    virtual void update() { m_player->update(); }
};

class MActorWrapper : public Actor {
    private:
    Monster *m_monster;
    
    public:
    MActorWrapper(Monster* monster)
    :m_monster(monster){}

    virtual void render() { m_monster->render(); }
    virtual void update() { m_monster->update(); }
};

Now, we can shove it all under the same type. Here, we just have to be aware of what all falls under this category. We should know the types in our game that should be updated and rendered. Thus we will end up having a list of types that should be updated and rendered without the need to be aware of their underlying types at all times.

ActorWrapper* actors [] = {
        new PActorWrapper(new Player()),
        new MActorWrapper(new Monster()),
        new CActorWrapper(new Cyclops()),
        new MActorWrapper(new Monster())
    };

Even now we have a bigger issue staring right at us! You do notice the new type called Cyclops there, right? It means we should also write a wrapper for the type Cyclops and any other type that is introduced further in the game. This introduces a much bigger problem of adding and removing numerous wrappers in our code.

But we already have a solution to avoid this new problem thrown at us. We can make use of template programming. Instead of writing a wrapper over individual types, we can template the wrapper like this.

template<typename T>
class ActorWrapper : public Actor {
    private:
    T *m_actor;
    
    public:
    ActorWrapper(T* actor)
    :m_actor(actor){}

    virtual void render() { m_actor->render(); }
    virtual void update() { m_actor->update(); }
};

Now, we have a single type called ActorWrapper and let the compiler generate the derived classes ActorWrapper<Player>, ActorWrapper<Monster> and ActorWrapper<Cyclops> for us. This can extend beyond these Player Monster and Cyclops. Any number of desired Actors can be created. We have to make sure that they define render and update functions in those classes.

This will let us do something like this.

ActorWrapper* actors [] = {
        new ActorWrapper(new Player()),
        new ActorWrapper(new Monster()),
        new ActorWrapper(new Cyclops()),
        new ActorWrapper(new Monster())
    };

The idiom Type Erasure gets its meaning from here because the crux of the idea is to implement an Actor class that unifies our other types like Player, Monster etc. We have unified all the Actors in our game by hiding them behind a custom interface called the ActorWrapper which just forwards the calls to the underlying types.

Clean up!

So far what we have discussed is the basic idea behind the concept of type erasure. As iterated, the type of the concrete class is erased by hiding it under a wrapper class that implements the concept (here the concept is an Actor). But the code can look messy when we create the objects of the wrapper class which will be like this new ActorWrapper(new Player()) or sometimes it may even look messier like this new ActorWrapper<Player>(new Player()). What is left is to hide all the messiness under a class to make it look cleaner outside, i.e. when it is used in other parts of the game.

In naming them, the class names ending with model and concept are quite standard ones. The class named Actor in the most recent code block captures the concept of an Actor, such as to render and to be able to update will be named as ActorConcept. The wrapper that implements the functions and forwards the call to the concrete types can be named as ActorWrapper or as ActorModel. Here we choose to go with the name ActorModel a widely accepted naming convention.

class Actor {
    private:
    
    class ActorConcept {
        public:
        virtual void render() = 0;
        virtual void update() = 0;
    };

    template<typename T>
    class ActorModel : public ActorConcept {
        private:
        T *m_actor;
        
        public:
        ActorModel(T* actor)
        :m_actor(actor){}

        virtual void render() { m_actor->render(); }
        virtual void update() { m_actor->update(); }
    };

    std::vector<ActorModel*> m_actors;

    public:
    template<typename T>
    void addActor(T* actor){
        m_actors.push_back(new ActorModel(actor));
    }

    void update(){
        for(auto actor: m_actors)
            actor->update();
    }
    
    void render(){
        for(auto actor: m_actors)
            actor->render();
    }
};

Once this Actor is implemented, in our code it can be used as follows.

    Player *p = new Player();
    Monster *m = new Monster();
    // continue to create other actors.

    Actor actors;
    actors.addActor(p);
    actors.addActor(m);
    //continue to add other actors.

    actors.update();
    //Whenever we want to update them
    
    actors.render();
    //Whenever we want to render them

Similar type erased concepts can be built for other requirements and the external system need not be aware of the underlying concrete types. This gives us greater flexibility in building large-scale systems with a lot of interdependencies.

More information?

  • For more info on C++ type erasure take a look at Andrzej’s series on type erasure (Part 1, Part 2, Part 3, Part 4).
  • There is an excellent talk on this concept by Klaus Iglberger available on youtube. <!—

    Pure Functions

    Many of us are already aware of free functions. Most of us use Pure Functions without knowing that they are Pure Functions. It can be loosely defined as a function that only looks at the parameters passed into it, and returns one or more computed values based on the parameters. It is one of the first things taught in class like Programming 101. Below is a very common pure function that almost everyone is aware of.

int add(int a, int b) {
   return a + b; 
}

The elegance of a pure function is, it leaves no footprint in the system. It does not mutate the input parameters, it maintains no internal state, and it doesn’t update any global state as well. There is a good article by John Carmack on functional programming here where he discussed in length about pure functions.

–>

I hope this post on Type Erasure was useful. Have any questions? Did I make any mistakes? Let me know on Twitter or Mastodon!

Tags

  • CPP
  • type erasure
  • virtual function
Back to Top ↑

Previous

Virtual Functions III

© 2023 Abilash Rajarethinam. Licensed under the WTFPL license.