Std::variant
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&