Send your request Join Sii

Inheritance is one of the core concepts of object-oriented programming (OOP) languages. It is a mechanism where you can derive a class from another class for a hierarchy of classes that share a set of attributes and methods.

It is a mechanism that allows developers to create a hierarchy between classes using an „is-a” relationship. The class being inherited from is called a parent class (base class) and the class inheriting is called the child class (derived class).

Inheritance allows programmers to create classes built upon existing classes and specify a new implementation while maintaining the same behaviours.

This article will demonstrate an alternative approach which will allow the creation of many polymorphic implementations with a minimal amount of code resulting in the same behaviour as in a well-known inheritance mechanism.

Runtime polymorphism with virtual functions

When speaking of polymorphism immediately the first things that come to our mind are virtual functions and v-tables (C++).

You declare a virtual function in a base class and then you override it in derived classes.

When you call such a function on a reference or a pointer to the base class, then the compiler will invoke the correct overload. In most cases, compilers implement this technique with virtual tables (v-tables). Each class that has a virtual method contains an extra table that points to the addresses of the member functions. Before each call to a virtual method, the compiler needs to look at v-table and resolve the address of a derived function at runtime (it is called late method binding).

An example below demonstrates a Vehicle base class with some interface and derived class (Pickup, Truck) which derive from it in a standard way.

Ryc. 1 - Modern approach in object-oriented design with std::variant and std::visit (C++17)
Fig. 3 1 - Modern approach in object-oriented design with std::variant and std::visit (C++17)
Fig. 2 1 - Modern approach in object-oriented design with std::variant and std::visit (C++17)

Pros and Cons of run-time polymorphism with virtual functions

Pros

  • it is very convenient to extend with new class,
  • object orientation allows deep hierarchies,
  • storing heterogeneous types in a single container is easy, just store pointers to the Base class or to the abstract class (interface).

Cons

  • virtual methods must be resolved before the call (performance overhead),
  • since you need a pointer to call a method, usually it means dynamic allocation (more performance overhead),
  • you need to modify the code of all classes in the inheritance structure.

Std::variant polymorphism

The class template std::variant represents a type-safe union. An instance of std::variant at any given time either holds a value of one of its alternative types or in the case of error – no value.

With this approach, there is no Vehicle class needed anymore as in the previous example. We can create more unrelated types here. The vehicles variable defines an object that can be a Pickup or Truck. In order to call a function we need a callable object and std::visit.

A struct callPrintName implements overloads for the call operator. Then std::visit takes the variant object and calls the correct overload. std :: visit is a powerful tool that allows you to call functions on the current type stored inside std :: variant. It can find the appropriate function overloads, and what’s more, it works for multiple variants at once.

Fig. 4 1 - Modern approach in object-oriented design with std::variant and std::visit (C++17)
Fig. 5 - Modern approach in object-oriented design with std::variant and std::visit (C++17)

This can be simplified and the callable object callPrintName can be replaced with generic lambdas if our variant subtypes share common interface.

Fig. 6 1 - Modern approach in object-oriented design with std::variant and std::visit (C++17)

What if we need some arguments? The main issues is that std::visit does not have a way to pass arguments into the callable object. It only takes a function object and a list of std::variant objects.

One option is to create a custom functor:

Fig. 7 768x596 1 - Modern approach in object-oriented design with std::variant and std::visit (C++17)

The other options to solve it by capturing the argument and forwarding it to a member function

Fig. 8 1 - Modern approach in object-oriented design with std::variant and std::visit (C++17)

The Overload pattern

The overload pattern is useful for wrapping multiple separate lambdas into an overload set. The code is shorter and there is no need to declare a structure that holds operator() overloads. In C++17 it required 2 lines of code to be implemented:

Fig. 9 768x46 1 - Modern approach in object-oriented design with std::variant and std::visit (C++17)

The first line is the overload pattern, the second is the deduction guide for it. The struct Overloaded

can have arbitrary many base classes. It derives from each class and brings the call operator(Ts::operator..) of each base class into the scope. The base classes need an overloaded call operator (Ts::operator()). Lambdas provide this call operator.

Example:

Fig. 10 1 - Modern approach in object-oriented design with std::variant and std::visit (C++17)

The overload pattern demonstrated several C++ techniques and allows to write shorter code.  C++17 simplifies syntax and reduces boilerplate code limiting potential errors.

Pros and Cons of std::variant polymorphism

Pros

  • value semantics,
  • no dynamic allocation,
  • no need for the base class,
  • class can be unrelated,
  • duck typing: gives extra flexibility, we can call functions from visitors with a different number of arguments, return types etc. On the contrary virtual functions need to have the same signature

Cons

  • need to know all types up front at compile time,
  • hard to add new types (changing type of variant and all visitors),
  • might waste memory,
  • each operation requires writing separate visitors,
  • passing parameters to std::visit is not as easy as with regular functions.

Summary

Modern C++(17) extends possibilities in terms of the runtime polymorphism approach. It leverages std::variant and std::visit to obtain that behaviour and might offer better performance and value semantics.

I’ve shown how you can use std::visit with multiple variants. Such a technique might lead to various “pattern matching” algorithms. You have a set of types, and you want to perform some algorithm based on the currently active types. It’s like doing polymorphic operations, but differently – as std::visit doesn’t use any v-tables.

***

If you want more detailed information on std::visit and std::variant and its internals please visit the link:

Rating:
Author

You might also like

More articles

Get an offer

If you have any questions or would like to learn more about our offer, feel free to contact us.

Send your request Send your request

Tomasz Ukraine Business Lead

Get an offer

Join Sii

Find the job that's right for you. Check out open positions and apply.

Apply Join Sii

Viktoriya Recruitment Specialist

Join Sii

SUBMIT

Цей контент доступний тільки в одній мовній версії.
Ви будете перенаправлені на головну сторінку.

Ви справді бажаєте залишити цю сторінку?