Replacing Unique_ptr With C++17’s std::variant — a Practical Experiment

uriotnews

Some time ago I wrote about a new way to implement runtime polymorphism which is based not on virtual functions but on std::visit and 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.

Intro

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 std::variant.

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 std::variant.

Ok, so let’s make some comparisons:

Size

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?

With unique_ptr you can just call a virtual function:

But how to do it with std::variant?

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 std::visit!

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 IAlgorithm class:

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?

Copyable Again

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!

With std::variant your classes have value semantics out of the box.

Source Code

All the code is available on my repo; there’s a separate branch for this experiment:

https://github.com/fenbf/ViAlg-Update/tree/variant

Summary

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 unique_ptr issues
  • 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.

This UrIoTNews article is syndicated fromDzone