std::optional & std::variant (C++17)
Type-safe nullable values and discriminated unions
std::optional
Represents an optional value - may or may not contain a value. Safer than using nullptr or sentinel values.
#include <optional>
#include <string>
// Function that might not return a value
std::optional<int> parse_int(const std::string& s) {
try {
return std::stoi(s);
} catch (...) {
return std::nullopt; // No value
}
}
// Usage
auto result = parse_int("42");
if (result) {
std::cout << "Value: " << *result << std::endl;
} else {
std::cout << "No value" << std::endl;
}
// With value_or
int value = parse_int("invalid").value_or(0); // Returns 0 if empty
// C++23: Monadic operations
auto doubled = parse_int("21")
.transform([](int n) { return n * 2; });std::variant
Type-safe union - holds one value from a fixed set of types. Replaces error-prone C unions.
#include <variant>
#include <string>
// Variant can hold either int, double, or string
using Value = std::variant<int, double, std::string>;
Value v1 = 42;
Value v2 = 3.14;
Value v3 = "hello";
// Check current type
if (std::holds_alternative<int>(v1)) {
std::cout << "It's an int: " << std::get<int>(v1) << std::endl;
}
// Type-safe access (throws std::bad_variant_access if wrong)
try {
std::string s = std::get<std::string>(v1); // Throws!
} catch (const std::bad_variant_access& e) {
std::cout << "Wrong type!" << std::endl;
}
// get_if returns pointer or nullptr
if (auto* p = std::get_if<double>(&v2)) {
std::cout << "Double value: " << *p << std::endl;
}std::visit
Apply a function to the currently held value in a variant.
// Overload pattern (C++17)
template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;
std::variant<int, double, std::string> v = "hello";
// Visit with lambda overloads
std::visit(overloaded {
[](int i) { std::cout << "int: " << i << std::endl; },
[](double d) { std::cout << "double: " << d << std::endl; },
[](const std::string& s) { std::cout << "string: " << s << std::endl; }
}, v);
// C++20: Simpler with generic lambda
std::visit([](auto&& arg) {
using T = std::decay_t<decltype(arg)>;
if constexpr (std::is_same_v<T, int>) {
std::cout << "int: " << arg << std::endl;
} else if constexpr (std::is_same_v<T, double>) {
std::cout << "double: " << arg << std::endl;
} else {
std::cout << "string: " << arg << std::endl;
}
}, v);Comparison
| Feature | std::optional<T> | std::variant<Ts...> |
|---|---|---|
| Represents | T or nothing | One of many types |
| Use Case | Nullable values | Type-safe unions |
| Empty State | std::nullopt | Always holds a value |
| Access | operator*, value() | std::get, std::visit |
Best Practices
- ✓Use optional instead of sentinel values (magic numbers/nullptr)
- ✓Use variant instead of void* + type tags or unions
- ✓Always check before dereferencing optional
- ✓Use std::visit with variant for type-safe dispatch
- ✗Don't use variant for types that share a common base (use polymorphism)