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.