Some time ago I wrote about a new way to implement runtime polymorphism which is based not on virtual functions but on
std::variant. Please have a look at this new blog post where I experiment with this approach on my home project. The experiment is more practical than artificial examples.
See advantages, disadvantages and practical code issues.
The new kind of runtime polymorphism is based on the fact that you can call
std::visit and then – at runtime – select the best matching overload for the active type in the variant:
Here’s a code sample which summarises this technique:
As you can see, we have two classes (unrelated, with just a similar member function name) and we “pack” them into a single
std::variant which can represent the first or the second type. Then when we want to call a given member function, we need to create a function object which handles both types (we can also create a generic lambda).
What are the advantages?
- No dynamic allocation to create a polymorphic class
- Value semantics, variant can be easily copied
- Easy to add a new “method”, you have to implement a new callable structure. No need to change the implementation of classes
- There’s no need for a base class, classes can be unrelated
- Duck typing: while virtual functions need to have the same signatures, it’s not the case when you call functions from the visitor. They might have a different number of argument, return types, etc. So that gives extra flexibility.
You can read more in: Bartek’s coding blog: Runtime Polymorphism with std::variant and std::visit
Let’s try to implement this approach on my project, is this as easy as it sounds on an artificial example?
What to Change in the Project
My project (sorting algorithms visualization, C++, WinApi, OpenGL, see at github) has a notion of algorithm manager class which has an “active” algorithm.
This active algorithm is just a unique pointer to
IAlgorithm – a base class for all available algorithms:
When a user changes the algorithm from the menu I need to update my pointer to base class so it points to a new algorithm.
Naturally, I selected virtual polymorphism as it’s easy to implement and work with. But this place is also a good candidate to experiment with
So I can create the following variant:
See Bartek’s coding blog: Everything You Need to Know About std::variant from C++17 if you want to know more about
Ok, so let’s make some comparisons:
The first thing that you can observe is that we don’t need any v-table pointers so that we can make class smaller (a bit):
After changing into variant:
The size between debug and release changes because of the string:
sizeof(string): 32 in Release and
40 in Debug.
One note: while the classes are smaller, you need to remember that std::variant composed of those unrelated types will need the max size of them, plus one field for the “discriminator” (currently active index).
We don’t have v-pointer so how can we call a function on that variant object? It’s not as easy as with a virtual dispatch.
How To Call a Member Function?
unique_ptr you can just call a virtual function:
But how to do it with
The basic idea is to use
std::visit and then pass a generic lambda that calls the proper member function:
In the above example, we perform runtime polymorphism by leveraging the
visit technique. In short, this function selects the best function overload based on the active type in the variant. Having a generic lambda allows us to have a simple way to call the same function for all possible types in the variant. This is, however, achieved through duck typing.
Problem: Passing Arguments
If you noticed, I put
?? in the generic lambda. This is because there’s no easy way to pass a parameter to the function from
To solve the issue we can capture the argument into out lambda:
The code is straightforward for simple built-in types, pointers or references, but it might be problematic when you have some larger objects (we’d like to forward the arguments, not copy them if possible).
Problem: Where to Store Lambdas?
Ok, but there might be several places where you want to call the
Init function on the current algorithm, for example in two or more member functions of the Algorithm Manager class. In that case, you’d have to write your lambdas twice, or store them somewhere.
You cannot store it (easily) as a static member of a class as there’s no auto type deduction available. You can keep them as static variables in a given compilation unit.
For my experiments I skipped lambdas and went for function objects that are declared in the
And now, in all places where you’d like to call a
member function of an algorithm you can just write:
Is that the best way?
CAlgManager had a
unique_ptr as a data member. To make this class copyable, I had to define copy/move constructors. But with
std::variant it’s not the case!
std::variant your classes have value semantics out of the box.
All the code is available on my repo; there’s a separate branch for this experiment:
Let’s compare the outcome, how about the positive side:
- value type, no dynamic memory allocation (no unique or smart pointers needed)
- copyable types, no
- no need to v-table, so smaller objects (if that’s important)
But how about the negative side:
- function objects – where to put them?
- need to add types to
using AlgorithmsVariant = std::variant<...explicitly
- duck typing sometimes can be painful, as the compiler cannot warn you about available methods of a given class (maybe this could be improved with concepts?)
- no override use, so the compiler cannot report issues with derived classes and their lack of full interface implementation
- no pure virtual functions – you cannot restrict the real “interface” on your derived classes
So… was this a right approach?
Not sure, as it was quite painful to get everything working.
It would be good to see other use cases where you have, for example, a vector of unique pointers. Replacing this to a vector of variant can reduce lots of small dynamic allocations.
Anyway, I did those experiments so you can see the “real” code and “real” use case rather than nice artificial examples. Hope it helps when you’d like to apply this pattern in your projects.
Let us know your experience in comments below the article.