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.
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.
This can be simplified and the callable object callPrintName can be replaced with generic lambdas if our variant subtypes share common interface.
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:
The other options to solve it by capturing the argument and forwarding it to a member function
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:
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:
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: