For compatibility reason to C the C++ language include the union construct which allows to manipulate several different types in a uniform manner. The reason I’m writing that this feature exist for compatibility reasons is that is almost completely useless when you’re doing object oriented programming, the only allowed types to be used with union are POD’s, non POD types (which have a non trivial constructor or destructor) are very uncomfortable to manipulate:
union { int n; std::string s; //Error } foo;
output:
variant.cpp:7:3: error: call to implicitly-deleted default constructor of ‘union (anonymous union at variant.cpp:4:1)’
} foo;
^
Since C++11, we can actually make this code work by either proving a defaulted default constructor for the non-POD type or provide a proper constructor (and destructor) to the union which will handle properly the initialization and destruction:
union foo { int n; std::string s; foo() { new (&s)std::string(); } ~foo() {}; };
This will require to construct properly all the union members, which might require a little of coding and still do not solve the problem of how properly destruct all the elements, say we have:
union foo { int n; std::string s; std::map<int,double> m; foo() { //What? Which one is the active member here?? new (&s)std::string(); new (&s)decltype(m)(); } ~foo() { //What? Clean std::string or std::map? }; };
Where is your God now? The point is that this is going to be very messy, hard to maintain and sooner or later will explode somewhere. Most importantly, this is almost unusable in real circumstances.
There are certain ways to handle such problems, for example by dynamically allocating objects and then handling them in a polymorphic way or other crazy void* style things, but those are just workarounds for problems which in C++17 we’re going to have solved for free! C++17 introduced a new type-safe way of handling types in a union fashion: std::variant.
std::variant, quick look.
As mentioned, std::variant is a type-safe union implemented as library container:
int main() { std::variant<int,std::string> foo; //Assign a value to the integer member foo = 69; //Extract the last assigned value: if( std::get<int>(foo) == 69 ) { std::cout << "Let's do it!\n"; } //Assign a value to the std::string member foo = "Ciao!"; std::cout<<std::get<std::string>(foo)<<std::endl; }
std::variant might contain any type, there is no dynamic memory allocation involved during the construction of the container and for any of its contained types, this mean that there’s no overhead in using it as if it was a plain union.
One quite interesting thing is that variant can contain types which are not default constructible:
struct bar { int n; bar(int v) : n{ v } {} }; int main() { //std::monostate is required here! std::variant<std::monostate,bar> foo; }
If you do not provide the std::monostate member the compilation will fail since bar is not default constructible, what std::monostate does is to provide a correctly behaving alternative in std::variant which make the std::variant itself default constructible, thus resolving the compilation error.
To be noted here that even if we’re able to construct the variant by exploiting the std::monostate type, any attempt to access bar will end up with the exception std::bad_variant_access thrown! One more interesting thing is that putting std::monostate not as first member will cause again a compilation error.
Std::variant will attempt to default-construct only the first type provided in the list (this is coherent with the way union works), for this reason if one wants to put non default-constructible types in the variant it must ensure that std::monostate is always listed as first element.
It is possible that std::variant to have non-values, or to be more precise, it might become valueless. This happen for example if during the initialization of the contained values an exception is thrown, eventually during a copy assignment operation or move assignment operation, this state can be ‘summoned’ as well if an exception is raised during an emplace operation.
Every element in the variant has it’s own index and each element can be accessed by this index or by one of the contained types, if more elements share the same type then the only secure way of accessing the elements is by the indexes:
int main() { //Four elements with idx: 0...2 std::variant<int,std::string,double> foo { 69, "Ciao!", 6.9 }; //Access by index int n = std::get<0>(foo); std::string s = std::get<1>(foo); double d = std::get<2>(foo); //Access by type int n = std::get<int>(foo); std::string s = std::get<std::string>(foo); double d = std::get<double>(foo); //Both those calls are raising std::bad_variant_access auto magic = std::get<3>(foo);//Out of range auto magic2 = std::get<char>(foo);//No 'char' in this variant }
To know which alternative is currently held by the variant the member function index() is provided, so in the last code example we have . To avoid mistakes when accessing the variant the non throwing accessor std::get_if can be used, if an access is not valid it will return a null pointer value instead of raising an exception.
std::visit.
C++17 is also providing a way of implementing the visitor pattern on variants (well, sort of…) by applying a certain function to one or more variants:
//Set of actions we want to perform struct visit_my_variant { bool operator()(int) { /* ... */ } bool operator()(std::string) { /* ... */ } bool operator()(double) { /* ... */ } } constexpr please_visit {}; int main() { std::variant<int,std::string,double> foo; //Will invoke please_visit and run the proper action std::visit(please_visit,foo); //Any callable object is allowed std::visit([](auto&& val) { //Needed to properly compare the types using T = std::remove_cv_t<std::remove_reference_t<decltype(val)>>; //std::is_same_v is a brand new helper variable if constexpr (std::is_same_v<T, int>) /* ... */ else if constexpr( std::is_same_v<T, std::string> ) /* ... */ else if constexpr( std::is_same_v<T, double> ) /* ... */ else /* crap */ } ); }
That is pretty powerful and is very versatile since std::visit can be used with any callable object, under the hood it make use of the homogeneous invocation semantic coming with the new standard, for some detail about this new cool stuff have a look here and here.
Conclusion.
If you remember the original problem I was raising in the first part of this post, then now you know that the important feature of std::variant is that is going to handle itself all the troubles related with constructing and destructing the entities it contains! And in doing so, it is absolutely type-safe!
An exciting period of new and interesting features is coming for us! We shall be very happy about this new language standard, in particular variants are going to be pretty useful when programming heavily object oriented (and generic) code.
Thanks for reading