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

unions are inherently error-prone due to the following main reasons:

  1. Unions are simple and have no way of knowing the current type.
  2. 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?

unions 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.