Variant
std::variant is a type-safe union introduced in C++17 that allows a variable to hold one of several different types at runtime. This is similar to union types in Java or TypeScript but provides compile-time type safety in C++.
Before talking about variant, it is essential to first mention unions, as unions are quite unique.
A traditional union is a data structure in C++ and C that allows storing different types of data in the same memory space. In other words, a union variable can store different types of data at different times, but it can only store one type at a time. Its memory size is equal to the size of its largest member.
When to Use Union?
Unions are suitable for memory-sensitive scenarios, especially in embedded systems or low-level system programming where manual memory optimization is necessary. They are typically used to represent multiple possible data types that do not coexist.
For example, measurement values might be represented in different units like meters, kilometers, or miles. These units are mutually exclusive since at any given time, a measurement will be represented in only one unit.
Variant Codes
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <iostream>
#include <variant>
#include <string>
int main() {
// Define a std::variant that can hold int, float, or std::string
std::variant<int, float, std::string> var;
// Assign different types of values to the variable
var = 81; // int
std::cout << "int: " << std::get<int>(var) << std::endl;
var = 3.3333f; // float
std::cout << "float: " << std::get<float>(var) << std::endl;
var = "Hello, variant!"; // std::string
std::cout << "string: " << std::get<std::string>(var) << std::endl;
// Check the currently held type
if (std::holds_alternative<std::string>(var)) {
std::cout << "The variant holds a string." << std::endl;
}
// Accessing the variable with the wrong type will throw an exception
try {
int value = std::get<int>(var); // Error, var currently holds std::string
}
catch (const std::bad_variant_access& e) {
std::cout << "Exception: " << e.what() << std::endl;
}
return 0;
}
output
When accessing a std::variant, if the type doesn’t match, an exception is thrown. This forces us to check the current type of the variant before accessing its value, which can be a bit cumbersome, especially when we just want to retrieve the current value. This leads us to the following approach.
Variant Pattern Matching
Pattern matching: std::variant can be used with std::visit for pattern matching, making access to different types more expressive and safe.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <variant>
struct Visitor {
void operator()(int i) const {
std::cout << "int: " << i << std::endl;
}
void operator()(float f) const {
std::cout << "float: " << f << std::endl;
}
void operator()(const std::string& s) const {
std::cout << "string: " << s << std::endl;
}
};
int main() {
std::variant<int, float, std::string> var = "Hello, variant!";
std::visit(Visitor{}, var); // Use visitor pattern to access the variant
return 0;
}
output
It’s worth noting that if the corresponding visitor function isn’t implemented but is used in the program, the compilation will fail directly.
Usage Scenarios
Replacing traditional unions: Traditional C++ unions cannot safely hold non-trivial types (e.g., types with destructors) and require manual type management, which is prone to errors. std::variant provides type safety and automatic management of destructors.
Handling multiple input types: If a variable needs to dynamically hold different types at runtime, std::variant is a great choice. For example, you can use it to represent a polymorphic return value or handle multiple types of input data.
If your situation aligns with these cases, consider using std::variant as a solution.




