Std::Variant' VS. Inheritance VS. Other Ways (Performance)

What are the advantages of using std::variant as opposed to traditional polymorphic processing?

So, is variant just a convenience?

No, they are different concepts. Main difference that on one side std::variant can work with unrelated types including builtins like int which is not possible with virtual functions directly. On another side std::variant must know types it is working with at compile time. For example it is possible to add a type with virtual function(s) by just linking additional object module without recompiling rest of the code or loading a shared library dynamically to existing application (you do not even have to restart the app) while with std::variant you must recompile code dealing with types std::variant contains.

Enforcing a common interface with std::variant without inheritance

The simplest and quite execution time optimal solution is have separate container for each type. Any example showing that Data Oriented Design is better then Object Oriented Programing is using this approach to show difference in performance.

Other way is to create some wrapper for variant:

class VisualElement
{
BaseElement* self;
std::variant<Circle, Image, Polygon> item;
public:

template<typename T, bool = std::is_base_of_v<BaseElement, T>>
VisualElement(const &T other) {
item = other;
self = &item.get<T>();
}

template<typename T, bool = std::is_base_of_v<BaseElement, T>>
VisualElement& operator=(const &T other) {
item = other;
self = &item.get<T>();
return *this;
}

bool hitTest(Point p) {
return self->hitTest(p);
// or use of std::visit and drop common interface ancestor.
}

Rect boundingRect() {
return self->boundingRect();
}
std::string uniqueId() {
return self->uniqueId();
}
};

Techniques for cutting down on verbosity when do polymorphism via std::variant rather than inheritance

Just use a visitor:

#include <variant>

struct state_a{};
struct state_b{};

struct actor
{
std::variant<state_a, state_b> state;
};

// basically no need to declare them as members.
void rotate(state_a, double deg)
{
// do a
}

void rotate(state_b, double deg)
{
// do b
}

void rotate(actor& a, double deg)
{
std::visit([deg](auto &state){ rotate(state, deg); }, a.state);
}

Here is a more scalable version with templates:
https://godbolt.org/z/58Kjx9h5W

// actor.hpp
#include <variant>

template <typename ... StatesT>
struct actor
{
std::variant<StatesT...> state;
};

template <typename StateT>
void rotate(StateT& s, double deg)
{
return rotate(s, deg);
}

template <typename ... StatesT>
void rotate(actor<StatesT...> &a, double deg)
{
std::visit([deg](auto &state){ rotate(state, deg); }, a.state);
}

// other headers
#include <iostream>

struct state_a{};
struct state_b{};

// basically no need to declare them as members.
void rotate(state_a, double deg)
{
// do a
std::cout << "rotating a\n";
}

void rotate(state_b, double deg)
{
// do b
std::cout << "rotating a\n";
}

int main()
{
auto actr = actor<state_a, state_b>{state_a()};
rotate(actr, 30);
}

Be advised, however, that "recursive" template <typename StateT> void rotate(StateT& s, double deg) will not work if you use a namespace for your states because of ADL.

You either declare rotate AND state_a in the same namespace.

std::variant vs pointer to base class for heterogeneous containers in C++

std::variant<A,B,C> holds one of a closed set of types. You can check whether it holds a given type with std::holds_alternative, or use std::visit to pass a visitor object with an overloaded operator(). There is likely no dynamic memory allocation, however, it is hard to extend: the class with the std::variant and any visitor classes will need to know the list of possible types.

On the other hand, BaseClass* holds an unbounded set of derived class types. You ought to be holding std::unique_ptr<BaseClass> or std::shared_ptr<BaseClass> to avoid the potential for memory leaks. To determine whether an instance of a specific type is stored, you must use dynamic_cast or a virtual function. This option requires dynamic memory allocation, but if all processing is via virtual functions, then the code that holds the container does not need to know the full list of types that could be stored.

std::variant vs std::any when type is move constructible

If you know the types, use std::variant. In that case you know all the possible types, and use std::variant::visit() to apply the visitor to the variants.

If you don't, use std::any. You can think about this when dealing with types that are generic. In other words, when the types are not known apriori.


Which one will give the least overhead?

std::variant, cannot use the heap. It's not allowed to. There is no way to actually have a structure containing a possible variant of itself without a dynamic allocation, as such a structure can easily be shown to be infinite in size if statically declared. Source.

In contrast, std::any may do so. As a result, the first will have faster construction and copy operations.



Related Topics



Leave a reply



Submit