I was browsing the internet and I came across the following statement

As I tried to point out in my answer, you can use templates in your runtime polymorphism case, but it will lead to unmaintainably bloated if/else branches.

- Stackoverflow user

I was wondering about the accuracy of it so i decided to put it to the test, and see different ways how we can use templates in our runtime polymorphic code.

Here is the code we will try to use in a runtime-polymorphic environment:

template < typename Container> void print_all (Container const & c, char const * separator = " " ) { for ( auto const & element : c) std::cout << element << separator; }

This piece of code runs through any object that can be iterated and prints the values it contains to std::cout . Here is an Example:

print_all(std::vector< int >{{ 1 , 2 , 3 , 4 , 5 }}, ", " ); // Output: // 1, 2, 3, 4, 5,

To verify the Stackoverflow user's assertion, let's try using this function with different types during runtime. We are going to discard all type information by using a void* and our own form of runtime type information: an int with agreed upon values.

Since we are relying on convention, we should try to document it as best as we can.

/** * Prints every element in a container * @param c : must be casted from one of * - vector <int> * - vector <float> * - vector <double> * @param type : indicates the type of the passed container * - 0 means vector <int> * - 1 means vector <float> * - 2 means vector <double> */ void print_container ( void * c, int type) { std::cout << "{ " ; if (type == 0 ){ print_all(* static_cast <vector< int >*>(c)); } else if (type == 1 ){ print_all(* static_cast <vector< float >*>(c)); } else if (type == 2 ){ print_all(* static_cast <vector< double >*>(c)); } else { std::cout << " - " } std::cout << "}

" ; }

Here are some examples of usage:

std::vector< int > vi {{ 1 , 2 , 3 , 4 , 5 }}; std::vector< int > vd {{ 5 , 4 , 3 , 2 , 1 }}; std::vector< char > vc {{ 1 , 3 , 2 , 4 , 5 }}; print_container(&vi, 0 ); // Output: // { 1 2 3 4 5 } print_container(&vd, 2 ); // Output: // { 5 4 3 2 1 } print_container(&vi, 2 ); // Output: // UNDEFINED BEHAVIOUR print_container(&vc, 0 ); // Output: // UNDEFINED BEHAVIOUR

This way, we can print different object types using the same function. provided, we pass the right number. If we don't, we hit undefined behaviour.

Just like we were warned by the Stackoverflow user, we ended up with long if/else chains and unsafe unmantainable code that leads to undefined behaviour, and there is nothing we can do about it. Right? Is this the best we can do? Well, of course not, that's what this post is about, so let's try to get this into a better shape.

For starters, let's try putting some type safety back into it. Some easy improvements are making our type indicator be an enum instead of an int, and making our pointer be a union of different pointer types instead of just void. We will still keep void as one of the types in our union because we will make use of it later on.

enum class ContainerType { vector_of_int, vector_of_float, vector_of_double }; union ContainerPointer { std::vector< int >* as_vector_of_int; std::vector< float >* as_vector_of_float; std::vector< double >* as_vector_of_double; void * as_void; };

Applying this, turns our function into:

/** * Prints every element in a container * @param c : pointer to the container we want to print * @param type : indicates the type of the passed container */ void print_container (ContainerPointer c, ContainerType t) { std::cout << "{ " ; switch (t){ case ContainerType::vector_of_int: print_all(*c.as_vector_of_int); break ; case ContainerType::vector_of_float: print_all(*c.as_vector_of_float); break ; case ContainerType::vector_of_double: print_all(*c.as_vector_of_double); break ; default : std::cout << " - " ; } std::cout << "}

" ; }

Which is at least a little bit beter.

Usage is roughly the same, but now it's a bit harder to mess up.

std::vector< int > vi {{ 1 , 2 , 3 , 4 , 5 }}; std::vector< double > vd {{ 5 , 4 , 3 , 2 , 1 }}; std::vector< char > vc {{ 1 , 3 , 2 , 4 , 5 }}; ContainerPointer cp; cp.as_vector_of_int = &vi; print_container(cp , ContainerType::vector_of_int); // Output: // { 1 2 3 4 5 } cp.as_vector_of_double = &vd; print_container(cp, ContainerType::vector_of_double); // Output: // { 5 4 3 2 1 } cp.as_vector_of_int = &vi; print_container(cp, ContainerType::vector_of_double); // Output: // UNDEFINED BEHAVIOUR cp.as_vector_of_int = &vc; print_container(cp, ContainerType::vector_of_int); // Output: // COMPILE-TIME ERROR (cant assign vector<char>* to vector<int>*)

From here, there a few different ways we can go, we are going to explore two different ways in this post.

First way

Now, why would we force a user of our type to carry out this annoying, error prone ritual every time they want to call a function on our type? Instead, we should try to encapsulate the implementation details and deal with this weird calling convention by ourselves.

We are going to do two things:

Making a new type that encapsulates the tagged-union idea that we are using



Pull the switch over different types into a method

struct TaggedContainerPointer { ContainerPointer pointer; ContainerType tag; /** * This method takes a polymorphic functor and applies it to the current * value that the TaggedContainerPointer holds. * * More simply: we are given a function that can take different types, so * we convert the pointer to the appropiate type and pass it on. */ template < typename Function> void visit (Function f) { switch (tag){ case ContainerType::vector_of_int: f(*pointer.as_vector_of_int); break ; case ContainerType::vector_of_float: f(*pointer.as_vector_of_float); break ; case ContainerType::vector_of_double: f(*pointer.as_vector_of_double); break ; default : std::cout << " - " ; } } };

With this in hand, the users of the type have a lot less typing -and a lot less worrying- to do.

Using this function is a lot easier:

/** * Prints every element in a container * @param c : pointer to container that will be printed */ void print_container (TaggedContainerPointer c) { std::cout << "{ " ; c.visit([]( auto const & x){ print_all(x); }); std::cout << "}

" ; }

The reason we have a generic lambda is that print_all is a templatized function and its type is not resolved when we pass it to TaggedContainerPointer::visit. The lambda fixes this because it is actually an object whose type is already resolved, the generic part is its operator(), which is fine since templatized method calls are allowed.

Ok this is nicer than if/else chains all over our code but it's a lot of work and we still have to ensure that the types match up (which we could fix with some more templates but that would be even more work, and even more confusion). Is there an easier way to do this?

Enter std::variant and std::visit

std::variant was introduced in C++17 to address this particular issue, it is a standard library componet that models a tagged union (The thing we did before) in a type-safe way.

The user code is very similar to what we just saw, but we don't have to worry about how it's implemented, and we can create new variant types without any additional effort.

It also has the added benefit that it is a well known and mostly well understood type that is part of the (C++17) standard library, which means it's easier to get permission to use in a corporate environment.

Here's how it look like:

using TaggedContainerPointer = std::variant< std::vector< int >*, std::vector< float >*, std::vector< double >* >; /** * Prints every element in a container * @param c : pointer to container that will be printed */ void print_container (TaggedContainerPointer c) { std::cout << "{ " ; std::visit([]( auto const & x){ print_all(*x); }, c); std::cout << "}

" ; }

Much simpler. A single line of code to define the type, and a single line to call it. That's how things should be.

Second way

The second way of doing templated runtime polymorphism comes from the realization that switch statements often get compiled down to jump tables, which we can emulate in our code and try to encapsulate.

A jump table is just an array of addresses, which we index into and jump to. Conceptually, it looks like this:

int index = /* some value */ ; void * jump_table [] = { /* some addresses*/ }; goto jump_table[index];

Though instead of goto and some made up addresses, we will use function pointers and function calls.

Here is how that would look like:

void print_container (ContainerPointer c, ContainerType t) { using TypePunnedFunction = auto (*)( void *) -> void ; static TypePunnedFunction jump_table[] = { (TypePunnedFunction)&print_all<std::vector< int >>, (TypePunnedFunction)&print_all<std::vector< float >>, (TypePunnedFunction)&print_all<std::vector< double >>, }; jump_table[ int (t)](c.as_void); }

See what i did there? I took some function pointers of different types and forced them to be of the same type by casting them to auto(*)(void*)->void (a function that takes a void pointer and returns nothing). This way, they can all be placed in the same array.

Now, I'm pretty sure that this is undefined behaviour, but let's ignore that and go with it for now. We will see how to make it safe later on. For now let's say we have yet another function we want to implement with the same calling semantics:

void print_sum_container (ContainerPointer c, ContainerType t) { using TypePunnedFunction = auto (*)( void *) -> void ; static TypePunnedFunction jump_table[] = { (TypePunnedFunction)&print_sum<std::vector< int >>, (TypePunnedFunction)&print_sum<std::vector< float >>, (TypePunnedFunction)&print_sum<std::vector< double >>, }; jump_table[ int (t)](c.as_void); }

This is not very nice: the code is messy, it has casts, for every function we write we have to add another table, and for every type we have to add and extra row to every table! We need to fix this.

We can start to simplify this by pulling out the jump tables to the global namespace, it should look like this:

using TypePunnedFunction = auto (*)( void *) -> void ; static TypePunnedFunction print_container_jump_table[] = { (TypePunnedFunction)&print_all<std::vector< int >>, (TypePunnedFunction)&print_all<std::vector< float >>, (TypePunnedFunction)&print_all<std::vector< double >>, }; static TypePunnedFunction print_sum_container_jump_table[] = { (TypePunnedFunction)&print_sum<std::vector< int >>, (TypePunnedFunction)&print_sum<std::vector< float >>, (TypePunnedFunction)&print_sum<std::vector< double >>, }; void print_container (ContainerPointer c, ContainerType t) { print_container_jump_table[ int (t)](c.as_void); } void print_sum_container (ContainerPointer c, ContainerType t) { print_sum_container_jump_table[ int (t)](c.as_void); }

What we have here are two parallel arrays where each one contains data related to a single function with different types: the indices and the types match up on each array.

How about we flip that around and turn it into arrays where each array has to do with a single type and each index is a particular function?

static TypePunnedFunction vector_of_int_jump_table[] = { (TypePunnedFunction)&print_all<std::vector< int >>, (TypePunnedFunction)&print_sum<std::vector< int >>, }; static TypePunnedFunction vector_of_float_jump_table[] = { (TypePunnedFunction)&print_all<std::vector< float >>, (TypePunnedFunction)&print_sum<std::vector< float >>, }; static TypePunnedFunction vector_of_double_jump_table[] = { (TypePunnedFunction)&print_all<std::vector< double >>, (TypePunnedFunction)&print_sum<std::vector< double >>, };

We now have arrays where the pointer at index zero points to the implementation of print_all for a particular type, and index one points to the implementation of print_sum for the same type.

This means we can change our code to this:

void print_container (ContainerPointer c, TypePunnedFunction* jump_table) { jump_table[ 0 ](c.as_void); } void print_sum_container (ContainerPointer c, TypePunnedFunction* jump_table) { jump_table[ 1 ](c.as_void); }

And as long as we pass the right pointer, this will work.

We can help make sure we get the right pointer by wrapping the ContainerPointer and TypePunnedFunction* in a struct and overriding the constructors to put the right pointers for the right types.

struct ContainerAndVTable { ContainerPointer c; TypePunnedFunction* vtable; ContainerAndTable (std::vector< int >* vi) { c.as_vector_of_int = vi; vtable = vector_of_int_jump_table; } ContainerAndTable (std::vector< float >* vf) { c.as_vector_of_float = vf; vtable = vector_of_float_jump_table; } ContainerAndTable (std::vector< int >* vd) { c.as_vector_of_double = vd; vtable = vector_of_double_jump_table; } };

What we have implemented here is a form of virtual function tables, commonly called vtables, which is how virtual methods are usually implemented. The implementation we have here is rather basic and not very safe, but luckily we can do better.

It's worth noting that at this point, it is not possible to call the function with an invalid type if we follow the interface.

Virtual Methods

Since C++ already has virtual methods that play nicely with the type system, we can use those instead. Here is how:

We first define the virtual functions we are going to use. C++ doesn't have free virtual functions so, instead, we use methods.

struct containerconcept { virtual void print () = 0 ; virtual void print_sum () = 0 ; virtual ~container () = default ; };

The way to write the implemenation of those methods is by creating a separate class and inheriting from this one, which then overrides the virtual methods.

struct vector_of_int : container_concept { std::vector< int > x; void print () override { print_all<std::vector< int >>(x); } void print_sum () override { print_sum<std::vector< int >>(x); } };

Ok, but where is the pointer?

What happened here is that now the vtable pointer is an implicit member of vector_of_int , it is dealt with by the language.

The language manages it for us so that whenever we pass a container_concept* , the right implementation can be looked up.

Of course, we don't want to write the same thing over and over again for vector<float> , vector<double> , and potentially many more. Instead, we are going to use templates.

template < typename T> struct container_impl : container_concept { T x; void print () override { print_all(x); } void print_sum () override { print_sum(x); } };

We could have used a pointer or reference here instead if we wanted this class to act as a view over an object that has already been created. I chose to store by value because it works better with what we are going to do next.

As it is, this is already a pattern that is familiar to many developers but we are going to go one step further: We are going to wrap this again in yet another struct. We are going to use a unique_ptr to manage ownership, and we are going to add a templated constructor and methods that forward the calls to the implementation.

struct container { struct container_concept { ... }; template < typename T> struct container_impl { ... }; std::unique_ptr<container_concept> value; template < typename T> container (T x) : value{std::make_unique<T>(std::move(x))} {} void print () { value->print(); } void print_sum () { value->print_sum(); } };

I know this last step is a lot but it's probably the most important one.

This is a particular form of the pImpl pattern that was popularized by Sean Parent, it very naturally encapsulates the concept of single-ownership and makes the use of templates in runtime-polymorphic code quite easy.

If you look closely you will notice that we just lost the restriction on our type. We can get it by back by using SFINAE or template specialization, but it might be worth not doing that. Writing it this way means that any type that meets the requirements of our implementation can be used, making our code much more general at basically zero cost.

Final words

I hope this makes it clear that templates can be used effectively in runtime polymorphism. All we need is access to the right abstractions, most of which are already part of the language, we just have to get to know them to be able to use them. And those that aren't, we can build ourselves.

I strive to show that nothing in the language or in the standard library is magic, anyone could have thought of it given the right circumstances, so don't be afraid to experiment and don't let yourself be limited by supersticion like "you can use templates in your runtime polymorphism case, but it will lead to unmaintainably bloated if/else branches".

Cheers