std::variant is a type-safe union that was introduced in C++17. It lets you store one value from a set of types in a single variable, much like a discriminated union in other languages. This feature is incredibly useful for modeling sum types, handling error states, or simply reducing the need for manual type checks.
1. Basic Declaration and Initialization
#include <variant>
#include <iostream>
#include <string>
int main() {
std::variant<int, std::string> data = 42; // holds an int
std::variant<int, std::string> text = std::string("hello");
std::cout << "int value: " << std::get<int>(data) << '\n';
std::cout << "string value: " << std::get<std::string>(text) << '\n';
}
- The variant is a template that takes an arbitrary number of types.
- The contained value can be retrieved with `std::get (variant)`. If the wrong type is requested, a `std::bad_variant_access` exception is thrown.
2. Visiting – The Safe Way to Handle All Cases
Instead of manually checking the active type, use std::visit with a lambda or a functor.
#include <variant>
#include <iostream>
#include <string>
int main() {
std::variant<int, std::string> data = "world";
std::visit([](auto&& val) {
using T = std::decay_t<decltype(val)>;
if constexpr (std::is_same_v<T, int>) {
std::cout << "int: " << val << '\n';
} else if constexpr (std::is_same_v<T, std::string>) {
std::cout << "string: " << val << '\n';
}
}, data);
}
std::visit will call the visitor with the currently stored value, allowing you to write type‑agnostic code.
3. Common Pitfalls
-
Ambiguous Overloads
If two types in the variant can be implicitly converted from the same expression, the compiler cannot decide which to use.std::variant<int, long> v = 5; // ambiguous (int or long)Use explicit construction:
std::variant<int, long> v = int{5}; -
Copying Variants with Large Types
std::variantstores all alternatives in the same memory block. If one alternative is very large, consider storing it by pointer or usingstd::unique_ptrinside the variant. -
Missing Default Case in
std::visit
If you forget a case, the visitor will still compile because the lambda is generic. But runtime errors can happen. Usestd::visitwith astd::variantthat contains all possible types you intend to handle.
4. Practical Use‑Case: Error Handling
Replace classic std::pair<bool, T> or custom error enums with a variant.
#include <variant>
#include <string>
#include <iostream>
struct Success {
int result;
};
struct Error {
std::string message;
};
using Result = std::variant<Success, Error>;
Result compute(int x) {
if (x < 0)
return Error{"Negative input"};
return Success{x * 2};
}
int main() {
auto res = compute(5);
std::visit([](auto&& val){
using T = std::decay_t<decltype(val)>;
if constexpr (std::is_same_v<T, Success>) {
std::cout << "Success: " << val.result << '\n';
} else {
std::cout << "Error: " << val.message << '\n';
}
}, res);
}
The variant cleanly encodes the “either” nature of the result, making the API easier to read and less error‑prone.
5. Extending with std::visit and Overload Sets
You can simplify visitors by combining overloaded lambdas:
template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts> overloaded(Ts...)->overloaded<Ts...>;
std::variant<int, std::string, double> v = 3.14;
std::visit(overloaded{
[](int i){ std::cout << "int: " << i << '\n'; },
[](const std::string& s){ std::cout << "string: " << s << '\n'; },
[](double d){ std::cout << "double: " << d << '\n'; }
}, v);
This pattern keeps your visitor code concise and readable.
6. Performance Considerations
- In‑place Storage:
std::variantkeeps all alternatives in a single buffer. If alternatives are large, consider using pointers orstd::unique_ptr. - Small-Object Optimization: For small types, the overhead is minimal; the variant is typically as fast as a union with manual tag handling.
- Exception Safety:
std::variantguarantees no resource leaks when the active alternative is swapped or destroyed, as long as the contained types are themselves exception‑safe.
7. Bottom Line
std::variant gives you a clean, type‑safe way to handle values that can be one of several types. It replaces many ad‑hoc approaches (tagged unions, unions with enums, or error‑code integers) and integrates seamlessly with C++’s pattern‑matching via std::visit. Embrace it for safer, clearer code when you need a sum type.
Happy coding!