Using std::variant in C++17: A Practical Guide

The std::variant class template, introduced in C++17, represents a type-safe union. An instance of std::variant holds a value of one of its specified alternative types at any given time, or it can be valueless under exceptional circumstances (via valueless_by_exception). It is a safer and more powerful alternative to traditional unions, supporting types like std::string and complex containers such as std::map.

Helper Types

std::monostate

std::monostate is a helper type that serves as a placeholder. It is especially useful when the first alternative type lacks a default constructor, enabling the variant to be default-constructible.

std::variant<std::monostate, int> v; // valid, index 0
std::cout << v.index(); // outputs: 0

std::bad_variant_access

This exception is thrown when you attempt to retrieve a value from a variant using std::get with a type or index that does not match the currently active alternative, or when visiting a variant that is valueless due to an exception.

#include <variant>
#include <iostream>

int main() {
    std::variant<int, float> v = 12;
    try {
        std::get<float>(v);
    } catch (const std::bad_variant_access& e) {
        std::cout << e.what() << '\n';
    }
}

Output:

bad_variant_access

std::variant_size and std::variant_size_v

These compile-time constants provide the number of alternatives in a possibly cv-qualified variant.

#include <variant>
#include <cstdio>

static_assert(std::variant_size_v<std::variant<>> == 0);
static_assert(std::variant_size_v<std::variant<int>> == 1);
static_assert(std::variant_size_v<std::variant<int, float, double>> == 3);

int main() {
    std::puts("All assertions passed.");
}

std::variant_alternative and std::variant_alternative_t

These utilities provide compile-time access to the type of a variant's alternative by index. cv-qualifiers on the variant are propagated to the extracted type.

#include <variant>
#include <type_traits>

using my_variant = std::variant<int, float>;
static_assert(std::is_same_v<int,   std::variant_alternative_t<0, my_variant>>);
static_assert(std::is_same_v<float, std::variant_alternative_t<1, my_variant>>);
static_assert(std::is_same_v<const int, std::variant_alternative_t<0, const my_variant>>);

int main() {}

Operations on std::variant

Construction

Specify the list of alternative types:

std::variant<uint32_t, double, std::string, int32_t> v;

Initializing with a value selects the best-matching type:

std::variant<uint32_t, double, std::string, int32_t> v{25};
std::cout << v.index(); // outputs: 3 (int32_t)

For multi-argument constructors or to disambiguate, use std::in_place_type or std::in_place_index:

std::variant<std::complex<double>> c{std::in_place_type<std::complex<double>>, 1.0, 2.0};
std::variant<int, int> v{std::in_place_index<1>, 42}; // second int

Important constraints:

  • std::variant cannot hold references, arrays, or void.
  • An empty variant (with no types) is ill-formed.

Accessing Values

Using std::get (may throw):

std::variant<uint32_t, double, std::string> v = 3.14;
try {
    double d = std::get<double>(v);
    std::string s = std::get<std::string>(v); // throws
} catch (const std::bad_variant_access& e) {
    // handle
}

Using std::get_if (returns pointer or null):

std::variant<int, float> v = 12;
if (auto* p = std::get_if<int>(&v)) {
    std::cout << *p; // 12
}

Using std::visit (preferred for efficiency):

Define a visitor with overloaded operator():

struct Visitor {
    void operator()(int i) const { std::cout << "int: " << i << '\n'; }
    void operator()(float f) const { std::cout << "float: " << f << '\n'; }
    void operator()(const std::string& s) const { std::cout << "str: " << s << '\n'; }
};

std::variant<int, float, std::string> v = 3.14f;
std::visit(Visitor{}, v);

Or use a polymorphic lambda with if constexpr:

std::visit([](auto&& arg) {
    using T = std::decay_t<decltype(arg)>;
    if constexpr (std::is_same_v<T, int>) {
        std::cout << "int: " << arg << '\n';
    } else if constexpr (std::is_same_v<T, float>) {
        std::cout << "float: " << arg << '\n';
    } else if constexpr (std::is_same_v<T, std::string>) {
        std::cout << "str: " << arg << '\n';
    }
}, v);

The if constexpr approach evaluates branches at compile time, making it more efficinet than runtime type checking. std::visit also acccepts multiple variant arguments.

Modifying Values

Via std::get (assigns to the active alternative):

std::variant<uint32_t, double, std::string> v;
std::get<double>(v) = 2.71;      // activates double alternative
std::get<0>(v) = 100;            // now activates uint32_t
std::get<std::string>(v) = "text"; // now activates string

Via std::get_if:

std::variant<int, double> v;
if (auto* p = std::get_if<double>(&v)) {
    *p = 3.14;
}

Summary

std::variant is a robust type-safe union that improves upon traditional unions by supporting complex types and providing compile-time checks. For accessing and mdoifying values, prefer std::visit combined with generic lambdas or overloaded visitors to achieve compile-time dispatch and cleaner code.

Tags: C++17 std::variant type-safe union C++ programming

Posted on Sun, 30 Aug 2026 16:39:09 +0000 by benjaminj88