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

Featurestd::optional<T>std::variant<Ts...>
RepresentsT or nothingOne of many types
Use CaseNullable valuesType-safe unions
Empty Statestd::nulloptAlways holds a value
Accessoperator*, 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)