Std::variant

From Chilipedia
Jump to: navigation, search

What is std::variant?

In C++17, std::variant was introduced and is basically a strongly typed union. It allows you to specify a list of states that the std::variant can be in. These states are actually types, such as int, float, double, MyClass. It can only be in one state at a time, so if you assign it an int on one line, then assign it a float on another, the int is no long able to be retrieved.


How to use std::variant

Std::variant has kind of a black-box approach in dealing with access to the data stored. While this does allow for some optimizations to be made in the form of compile time knowledge for the compiler, it makes it non-trivial for the user to access. Luckily, there are options, from safe but requires a bit of boiler-plate to not so safe but trivial. Look at the examples below.

First, the safe way. There are a few versions that pretty much do the same thing. Since the compiler knows the type stored in an std::variant, you can use the std::visit function to have the compiler choose which function to handle the variant.

#include <variant>  // for std::variant, std::visit and an overload of std::get that works with std::variant
#include <iostream> // for std::cout

void PrintNumber( const int _number )
{
     std::cout << _number << '\n';
}
void PrintNumber( const float _number )
{
     std::cout << _number << '\n';
}
void PrintNumber( const double _number )
{
     std::cout << _number << '\n';
}

int main( int ArgsC, char* ArgV[] )
{
     std::variant<int, float, double> number;

     number = 42;  // number is now considered an int
     std::visit( &PrintNumber, number );  // The PrintNumber( int ) overload should be called
     
     number = 3.14159f;  // number is now considered a float
     std::visit( &PrintNumber, number );  // The PrintNumber( float ) overload should be called

     number = 2.5  // number is now considered a double
     std::visit( &PrintNumber, number );  // The PrintNumber( double ) overload should be called

     return 0;
}

Version two using a function object as the 'visitor'.

#include <variant>  // for std::variant, std::visit and an overload of std::get that works with std::variant
#include <iostream> // for std::cout

struct PrintNumber
{
     void operator()( const int _number )
     {
          std::cout << _number << '\n';
     }
     void operator()( const float _number )
     {
          std::cout << _number << '\n';
     }
     void operator()( const double _number )
     {
          std::cout << _number << '\n';
     }
};

int main( int ArgsC, char* ArgV[] )
{
     std::variant<int, float, double> number;

     number = 42;  // number is now considered an int
     std::visit( PrintNumber(), number );  // The PrintNumber::operator()( int ) overload should be called
     
     number = 3.14159f;  // number is now considered a float
     std::visit( PrintNumber(), number );  // The PrintNumber::operator()( float ) overload should be called

     number = 2.5  // number is now considered a double
     std::visit( PrintNumber(), number );  // The PrintNumber::operator()( double ) overload should be called

     return 0;
}

Version 3 using lambdas for the 'visitor'

#include <variant>  // for std::variant, std::visit and an overload of std::get that works with std::variant
#include <iostream> // for std::cout

int main( int ArgsC, char* ArgV[] )
{
     auto PrintNumber = []( auto _number )
          {
               using type = std::decay_t<decltype( _number )>;
               if constexpr( std::is_same_v<type, int> )
               {
                    std::cout << _number << '\n';
               }
               else if constexpr( std::is_same_v<type, float> )
               {
                    std::cout << _number << '\n';
               }
               else if constexpr( std::is_same_v<type, double> )
               {
                    std::cout << _number << '\n';
               }
          };

     std::variant<int, float, double> number;
     
     number = 42;  // number is now considered an int
     std::visit( PrintNumber, number );  // The if constexpr( std::is_same_v<type, int> ) branch should be used
     
     number = 3.14159f;  // number is now considered a float
     std::visit( PrintNumber, number );  // The else if constexpr( std::is_same_v<type, float> ) branch should be used

     number = 2.5  // number is now considered a double
     std::visit( PrintNumber, number );  // The else if constexpr( std::is_same_v<type, double> ) branch should be used

     return 0;
}

Do note that for each type in the variant, you must have a case handling the 'visit' or you'll get a compile time error, and it is nowhere near helpful in determining what case you missed, but not impossible.

Next is a safe way, but can be less efficient depending on your accuracy in knowing the type being stored.

#include <variant>  // for std::variant, std::visit and an overload of std::get that works with std::variant
#include <iostream> // for std::cout

void PrintNumber( std::variant<int, float, double>& _number )
{
     if( const int* myInt = std::get_if<int>( &_number ); myInt != nullptr )
     {
          std::cout << *myInt << '\n';
     }
     else if( const float* myFloat = std::get_if<float>( &_number ); myFloat != nullptr )
     {
          std::cout << *myFloat << '\n';
     }
     else if( const double* myDouble = std::get_if<double>( &_number ); myDouble != nullptr )
     {
          std::cout << *myDouble << '\n';
     }
}

int main( int ArgsC, char* ArgV[] )
{
     std::variant<int, float, double> number;
     
     number = 42;  // number is now considered an int
     PrintNumber( number );

     number = 3.14159f;  // number is now considered a float
     PrintNumber( number );

     number = 2.5  // number is now considered a double
     PrintNumber( number );

     return 0;
}

Unlike std::tuple, variants cannot hold multiples of the same type. Each type in the template parameter list must be unique.

Finally, the unsafe way.

#include <variant>  // for std::variant, std::visit and an overload of std::get that works with std::variant
#include <iostream> // for std::cout

int main( int ArgsC, char* ArgV[] )
{
     std::variant<int, float, double> number;
     
     number = 42;  // number is now considered an int
     int& myInt = std::get<int>( number );
     myInt = 69;
     std::cout << myInt << '\n';

     number = 3.14159f;  // number is now considered a float
     float& myFloat = std::get<float>( number );
     myFloat = 0.707f
     std::cout << myFloat << '\n';

     number = 2.5  // number is now considered a double
     double& myDouble = std::get<double>( number );
     myDouble = 419.999999999
     std::cout << myDouble << '\n';

     return 0;
}

This example, made it safe by using the retrieved type directly after assignment, but after that, the user may not know what state the variant is in and calling std::get on the wrong type will throw a std::bad_variant_access exception. The above method of assigning the value, then retrieving a reference to the underlying type allows you to modify the state of the object from within without having to setup visitors just for initialization.


Requirements for std::variant

  • The list of types must be unique, a variant cannot be declared with two ints and a float
  • The types in a variant must be default constructable or the use of std::monostate can be set as the first type in the parameter list
     std::variant<std::monostate, MyComplexClass, MyComplexClass1> data;
  • Types in std::variant parameter list, must not be forward declared types. The compiler must be able to determine whether or not the types in the list are default constructable before instantiating a variant of those types.
  • The list may not contain reference types: int&, const MyCompolexClass&