std::variant in C++
Do not miss this exclusive book on Binary Tree Problems. Get it now for free.
std::variant was added in C++17 to support the sum type. It adds more functionality to union and hence is a safer version of it.
Sum types and product types
A type defines the set of values and the permissible operations on those values. For example it might make sense to concatenate two strings.
Types are usually classified into different typeclasses such as enumerable, comparable etc. The types of our interest are composite types that have sum type and product type.
The Sum types like T1+T2 consists of all the values of T1 plus all the values of T2. Sums are often called Unions. If the alternatives are labeled (or named), then we call them tagged unions. In general we can have sum of any size: T1+T2+ … +Tn.
The Product types like T1×T2 consists of all pairs (x,y) where x has type T1 and y has type T2. In general we can have products of any size: T1×T2× … ×Tn. In general, product types are called tuples. If the components are labeled (or named), then we call them records.
std::variant as a safer union
The sum types are unions and tagged unions. In C++ we have a union
class type which represents the union, and std::variant
class template which represents the tagged union.
Problems with union
union
s are inherently error-prone due to the following main reasons:
- Unions are simple and have no way of knowing the current type.
- There is no proper constructor or destructor call when type is changed. There can be a user-defined constructor and destructor.
Unions are simple and have no way of knowing the current type.
A union has no associated indicator as to which member (if any) it holds, so we have to keep track of it. Not keeping track usually lead to type errors.
For example:
union Value{
int i;
double d;
};
Value v;
v.d = 98.7654; // v now holds a double
defines a union with int
or double
type. But if a user accesses an int
it leads to undefined behaviour and there is no error message.
cout << v.i << endl; // bad access: 1346901744
v
holds a double
(value 98.7654), but we read it as an int
(1346901744 which is integer representation of bit pattern for 98.7654)
There is no proper constructor or destructor call when type is changed.
There can be a user-defined constructor and destructor. But this has a problem when we cannot determine which type is active.
union U
{
string s;
vector<int> v;
~U() { } // what to delete here?
};
In order to be able to tell, we need some information about which type of value is being held currently in the union. Thus we need to keep track of which type is currently active.
Thus handling the assignment of values and destructor are difficult.
std::variant
Determining the current type held by std::variant
std::variant
provides a better union
. It can be considered to be a wrapper class for union
with added functionality.
std::variant
provides observers: index()
and valueless_by_exception()
index()
which returns the index of the type being currently active. for example an instance of type variant<int, string, double>
may have an int
at index 0
, string
at index 1
, and double
at index 2
, although only one would be active at any particular time.
valueless_by_exception()
which returns false if there is a value being held in the variant. If some exception is raised while changing the type of the variant, then this function is useful to determine that the variant will be valueless.
variant has constructor and destructors provided
For the second reason, std::variant
provides a constructor and destructor. The constructor provides various ways to construct a variant object. The destrcutor allows calling appropriate destructor on the type that is currently active.
What about memory?
union
s save memory because they allow a single piece of memory to be used for different types of objects at different times. Consequently, they can be used to save memory when we have several objects that are never used at the same time.
std::variant
uses the memory similar to the union
. It will take the max size of the underlying types. However, std::variant
will take a little extra space for all the type-safe functionality provided to us. But the extra memory space is well-spent.
One additional thing is that std::variant
does not dynamically allocate memory for the values. Any instance of std::variant
at any given time either holds a value of one of its alternative types, or it holds no value at all. The contained value, is allocated within the storage of the variant object and will not use additional storage, such as dynamic memory, to allocate the value. The contained value will be suitably aligned for all types.
Usage
Construction
To create a variant that can store an int or a string. Here the first alternative (int
) is assigned a default value of 0
std::variant<int, std::string> var;
we can assign values like
std::variant<int, double> var{1}; // 1 is int literal
var = 2.34; // double value
additionally there are copy-constructors and move constructor among others.
variant<string, int> var{"str"};
variant<string, int> u = var;
variant<string, int> v = variant<string, int>("str");
Some important functions
index()
index()
allows us to know the index of the type that is active.
for example:
variant<int, string, double> var = "str";
cout << var.index() << endl; // 1
var = 123;
cout << var.index() << endl; // 0
valueless_by_exception()
valueless_by_exception()
returns false
iff the variant holds a value.
for instance as in the following example:
struct S {
operator int() { throw 42; }
};
// ...
variant<float, int> v{12.0f};
cout << boolalpha << v.valueless_by_exception() << endl; // false
try{
v.emplace<1>(S()); // v may be valueless
}
catch(...){
}
cout << v.valueless_by_exception() << endl; // true if valueless
visit()
visit()
allows us to access the contents of a variant. It takes a visitor which is a polymorphic function.
std::variant<int, std::string> v{78};
visit( [](auto&& elem){std::cout << elem << endl; }, v );
in this example a polymorphic lamda is used.
holds_alternative()
holds_alternative()
allows us to determine if the instance of std::variant
holds a value of alternative types.
for example:
std::variant<int, std::string> var = "abc";
std::cout << std::boolalpha
<< std::holds_alternative<int>(var) << endl // false
<< std::holds_alternative<int>(var) << endl; // true
get<T>()
get<T>
allows us to access values based on the index or the type.
Consider an example for access by index and type
variant<int, float> v{99};
// access by index
cout << (get<0>(v)) << endl; // 99
// access by type
cout << (get<int>(v)) << endl; // 99
Example
Sample code that shows usage of std::variant
and its behaviour:
#include<iostream>
#include<variant>
using namespace std;
int main()
{
cout << boolalpha; // print "true" or "false" instead of 1 or 0 respectively
variant<int, string> var; // value-initializes the first type
cout << holds_alternative<int>(var) << endl; // true: holds an int
cout << (var.index() == 0) << endl; // true: first type is active
cout << get<int>(var) << endl; // 0: var was initialized to default int 0
variant<int, string> var2{"hello"}; // initialize string to "hello"
cout << (var2.index() == 1) << endl; // true
if(holds_alternative<string>(var2)){ // if var2 holds a string
cout << get<string>(var2) << endl; // "hello"
}
return 0;
}
Conclusion
As we saw a std::variant
object holds and manages the lifetime of a value. It will hold one of its alternative types. It lets us determine the currently held type of value by providing functions and allows construction and destruction. It thus adds more functionality to union
and hence is a safer version of it.
Sign up for FREE 3 months of Amazon Music. YOU MUST NOT MISS.