std::variant in C++

Cengizhan Varlı
7 min readJan 27, 2024

--

std::variant represents a type-safe union.

What does this sentence mean?

What is type-safe? and What is union?

If you have worked with C language, you have definitely used union. In the C programming language, a union is a composite data type that allows you to store different types of data in the same memory location. Like a structure , a union can have multiple members, but unlike a structure, a union allocates only enough memory to hold its largest member. This means that all members of a union share the same memory space.

What a quite useful !

Let’s take a look at the C code below.

#include <stdint.h>
#include <stdio.h>

union
{
struct
{
uint8_t low : 4;
uint8_t high : 4;
}nibles;

uint8_t bytes;
}myByte;

int main()
{
myByte.bytes = 0xAB ;

printf("High Nible : 0x%X\n",myByte.nibles.high);
printf("Low Nible : 0x%X\n",myByte.nibles.low);

return 0;
}
Output

As seen above, it allocates as much memory as the longest element in the union. So 1 byte. It puts the variables written into it at the same address. In this way, if the variable named bytes changes, the other element of the union, the structure named nibles, also changes.

If you are using the C programming language, using union will be very useful. If you weren’t using unions, you’d have to implement bitwise operations for the example above.

Anyway, let’s get back to C++

If you are using C++, we should not think as using C !

Let’s look at the example below;

#include <iostream>

union MyUnion {
int intValue;
double doubleValue;
};

int main() {
MyUnion myUnion;

myUnion.doubleValue = 3.14;

std::cout << "Integer Value: " << myUnion.intValue << "\n";

return 0;
}

What do you think will happen?

We said that it puts the variables in the union at the same address. It contains int and double variables. We assigned 3.14 to the double variable. And we tried to access int variable.

Here is the result;

Output

What is that?

It’s like there’s something missing in our lives, like type-safe !

I think I shouldn’t be able to access int value. It should have returned me exception when I wanted to access it.

std::variant, which came into our lives with C++17, offers us type-safe union.

What is std::variant?

std::variant is a C++17 feature that provides a type-safe union. It is a part of the C++ Standard Library and is defined in the <variant>header.

In C++, a union allows you to store different types of data in the same memory location, but it does not provide type safety. std::variant is an improvement over traditional unions, as it ensures type safety and provides a convenient way to work with values of different types.

Let’s test the type safe feature first.

#include <variant>
#include <iostream>

int main() {
std::variant<int, double> myVariant;

myVariant = 42; /* Store an int */

std::cout << std::get<double>(myVariant) << std::endl;

return 0;
}
Output

We created a std::variant that holds int and double. We assigned the value 42 to the int variable. Then we tried to access the double variable. and we got the output as we expected. This is it !

That’s type-safe !

Let’s look at another example about usage std::variant.

#include <iostream>
#include <variant>
#include <string>

int main() {
/* Define std::variant; it can hold either an int or a std::string value */
std::variant<int, std::string> myVariant;

/* Assign an int to std::variant */
myVariant = 42;

/* Assign a std::string to std::variant */
myVariant = "Hello, Variant!";

/* Retrieve and use the value from std::variant */
try {
/* It's important to check which type is stored in std::variant before retrieving the value */
if (std::holds_alternative<int>(myVariant))
{
std::cout << "Value as int: " << std::get<int>(myVariant) << std::endl;
}
else if (std::holds_alternative<std::string>(myVariant))
{
std::cout << "Value as string: " << std::get<std::string>(myVariant) << std::endl;
}
else
{
std::cout << "Unknown type!" << std::endl;
}
} catch (const std::bad_variant_access& e)
{
std::cerr << "Error: " << e.what() << std::endl;
}

return 0;
}
Output

In the main function, a std::variant named myVariant is defined, capable of holding either an int or a std::string. An int value (42 in this case) is assigned to the std::variant. And then a std::string ("Hello, Variant!") is assigned to the same std::variant. This demonstrates the flexibility of std::variant to hold values of different types. A try-catch block is used to handle potential exceptions when accessing the value stored in the std::variant. It checks the type of the stored value using std::holds_alternative and then extracts and prints the value accordingly. If an unknown type is encountered, it prints an error message. If an exception occurs (e.g., trying to access the wrong type), it catches the exception and prints an error message using std::cerr.

std::variant member functions

(public member function)

1. Default Constructor:

std::variant<int, double, std::string> myVariant; /* Default construction */

2. Assignment Operator:

Assigns a new value to the variant.

std::variant<int, double> myVariant;
myVariant = 42; /* Assigns an int */

3. Index

Returns the zero-based index of the alternative held by the variant

   std::variant<int, double, std::string> myVariant = 3.14;

std::cout << myVariant.index() << "\n"; /* Output 1 */

myVariant = "Cengizhan";

std::cout << myVariant.index() << "\n"; /* Output 2 */

4. Valueless by Exception:

valueless_by_exception() checks if the variant is in a valueless state (i.e., has no value).

std::variant<int, double> myVariant;
if (myVariant.valueless_by_exception()) {
/* Handle the case where the variant has no value */
}

5. Swap

std::swap() swaps the values of two std::variant objects of the same type.

std::variant<int, double> var1 = 42;
std::variant<int, double> var2 = 3.14;
std::swap(var1, var2);

6. Emplace

emplace() constructs the value in-place using the provided arguments.

std::variant<int, std::string> myVariant;
myVariant.emplace<std::string>("Hello");

std::variant Advantages

1. Type Safety:

As said before, Traditional unions can lead to runtime errors when accessing the wrong type. std::variant provides type safety at compile-time.

std::variant<int, double, std::string> myVariant = 42;
// Type-safe access
std::cout << std::get<int>(myVariant) << std::endl;

2. Eliminates Manual Type Checking and Casting:

With std::variant, you don't need to manually check or cast types, leading to cleaner and safer code.

std::variant<int, double> myVariant = 3.14;
// No need for manual type checks and casts
std::cout << std::get<double>(myVariant) << std::endl;

3. Improved Readability:

Clearly expresses the intention when a variable can have one of several types.

std::variant<int, double, std::string> myVariant = "Hello";
// Improved readability
if (std::holds_alternative<std::string>(myVariant)) {
std::cout << std::get<std::string>(myVariant) << std::endl;
}

4. Avoids Undefined Behavior:

Prevents undefined behavior by enforcing type safety.

std::variant<int, double> myVariant = 42;
// Accessing the wrong type would result in std::bad_variant_access
std::cout << std::get<double>(myVariant) << std::endl;

5. Error Handling:

Useful in error handling scenarios where a function can return different types of results.

std::variant<int, std::string> result = performOperation();
// Handle different result types in a type-safe manner
if (std::holds_alternative<int>(result)) {
// Handle integer result
} else if (std::holds_alternative<std::string>(result)) {
// Handle string result
}

6. Compatibility with Standard Library Algorithms:

Compatible with standard algorithms, allowing easy integration into existing codebases.

std::vector<std::variant<int, double, std::string>> data;
// Can use standard algorithms with std::variant
std::for_each(data.begin(), data.end(), [](auto& item) {
std::visit([](auto&& arg){ std::cout << arg << std::endl; }, item);
});

In summary, std::variant enhances code safety, readability, and maintainability by providing a type-safe way to work with values of different types. It excels in scenarios where flexibility in representing multiple types in a single variable is required.

State Machine implementation with std::variant

As you know, only one value is kept at a time with std::variant. It is possible to write state machine code by taking advantage of this feature.

#include <iostream>
#include <variant>

/* Define the states */
struct OpenState {};
struct ClosedState {};

/* Define the events */
struct OpenEvent {};
struct CloseEvent {};

/* Define the state machine */
using DoorState = std::variant<OpenState, ClosedState>;

/* Define the state transition function */
DoorState transition(DoorState currentState, const OpenEvent&) {
if (std::holds_alternative<ClosedState>(currentState)) {
std::cout << "Opening the door.\n";
return OpenState{};
} else {
std::cout << "The door is already open.\n";
return currentState;
}
}

DoorState transition(DoorState currentState, const CloseEvent&) {
if (std::holds_alternative<OpenState>(currentState)) {
std::cout << "Closing the door.\n";
return ClosedState{};
} else {
std::cout << "The door is already closed.\n";
return currentState;
}
}

int main() {
DoorState doorState = ClosedState{}; /* Initial state: Closed */

/* Events triggering state transitions */
doorState = transition(doorState, OpenEvent{});
doorState = transition(doorState, CloseEvent{});
/* Try closing the already closed door */
doorState = transition(doorState, CloseEvent{});

return 0;
}
Output

In this example, DoorState is a std::variant representing the possible states of the door. The transition functions handle the state transitions based on the events (OpenEvent and CloseEvent). The std::holds_alternative function is used to check the current state before performing a transition.

--

--