Std::variant

From Chilipedia
Revision as of 03:59, 18 October 2018 by Albinopapa (Talk | contribs) (Created page with " == What is <code>std::variant</code>? == In C++17, std::variant was introduced and is basically a strongly typed <code>union</code>. It allows you to specify a list of state...")

(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
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.

  1. include <variant> // for std::variant, std::visit and an overload of std::get that works with std::variant
  2. 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'.

  1. include <variant> // for std::variant, std::visit and an overload of std::get that works with std::variant
  2. 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'

  1. include <variant> // for std::variant, std::visit and an overload of std::get that works with std::variant
  2. 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.

  1. include <variant> // for std::variant, std::visit and an overload of std::get that works with std::variant
  2. 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.

  1. include <variant> // for std::variant, std::visit and an overload of std::get that works with std::variant
  2. 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