библиотека — типы общего назначенияlibrary — general-purpose types

std::variant

Типобезопасное объединение (discriminated / tagged union), хранящее ровно одно значение одного из перечисленных типов одновременно, плюс информацию о том, какой тип активен прямо сейчас.A type-safe discriminated (tagged) union, holding exactly one value of one of the listed types at a time, plus information about which type is currently active.

часто используетсяcommonly used частый вопрос: variant vs union vs anycommon question: variant vs union vs any

Что этоWhat it is

std::variant<T1, T2, ...> хранит значение ровно одного из типов T1, T2, ... в конкретный момент времени, и всегда знает, какой именно — в отличие от сырого union. Доступ к активному значению — через std::get<T>(v) (кидает std::bad_variant_access, если активен другой тип), std::get_if<T>(&v) (возвращает указатель или nullptr, без исключений), либо std::visit — диспетчеризация по активному типу через вызываемый объект.std::variant<T1, T2, ...> holds a value of exactly one of the types T1, T2, ... at any given moment, and always knows which one — unlike a raw union. Access to the active value is through std::get<T>(v) (throws std::bad_variant_access if a different type is active), std::get_if<T>(&v) (returns a pointer or nullptr, no exceptions), or std::visit — dispatch on the active type via a callable object.

до C++17 — сырой union или ручной tagged structbefore C++17 — a raw union or a hand-rolled tagged struct
union Raw {
    int i;
    double d;
    // std::string s; — нельзя без ручного placement new/~string()// std::string s; — impossible without manual placement new/~string()
};

struct Tagged {
    enum { INT, DOUBLE } tag;  // ручной тег — легко забыть обновить// a manual tag — easy to forget to update
    union { int i; double d; };
};
// обращение не к тому члену union — UB, компилятор не проверяет// accessing the wrong union member is UB, the compiler doesn't check
C++17 — std::variantC++17 — std::variant
std::variant<int, double, std::string> v = 42;

if (auto* p = std::get_if<int>(&v)) {
    use(*p);  // безопасно — тип проверен явно// safe — the type was checked explicitly
}

v = std::string("now a string");  // смена активного типа отслеживается автоматически// the active type change is tracked automatically
std::get<int>(v);  // кидает std::bad_variant_access — активен string, не int// throws std::bad_variant_access — string is active, not int

Зачем ввелиWhy it was introduced

Сырой union в C++ решает задачу «одна из нескольких альтернатив» лишь частично, оставляя две дыры:A raw union in C++ solves the "one of several alternatives" problem only partially, leaving two gaps:

01

union не отслеживает, какой член активенunion doesn't track which member is active

Записать в u.i, а потом прочитать u.d — undefined behavior, но компилятор это не проверяет и не запрещает. Ответственность за то, чтобы помнить «какой сейчас член активен», целиком лежит на программисте — обычно через отдельный ручной enum-тег, который легко забыть синхронизировать при присваивании.Writing to u.i and then reading u.d is undefined behavior, but the compiler neither checks nor forbids it. Remembering "which member is currently active" is entirely the programmer's responsibility — usually via a separate hand-written enum tag, which is easy to forget to keep in sync on assignment.

02

union не поддерживает нетривиальные типы без ручного вмешательстваunion doesn't support non-trivial types without manual intervention

Тип с нетривиальным конструктором/деструктором (например, std::string или std::vector) нельзя просто положить в union — компилятор не знает, какой конструктор вызвать при инициализации union и какой деструктор при его уничтожении. Единственный выход — вручную вызывать placement new и явный деструктор в нужные моменты, что легко сделать неправильно.A type with a non-trivial constructor/destructor (like std::string or std::vector) can't simply be placed into a union — the compiler doesn't know which constructor to call when initializing the union or which destructor to call when destroying it. The only way out is manually invoking placement new and an explicit destructor at the right moments, which is easy to get wrong.

03

variant решает обе проблемы разомvariant solves both problems at once

variant хранит собственный внутренний индекс активного типа (доступен через v.index()) и вызывает правильные конструкторы/деструкторы автоматически при каждой смене значения — работает с любыми типами, включая классы с нетривиальными спецсредствами, без единой ручной placement-new операции со стороны пользователя.variant maintains its own internal index of the active type (available via v.index()) and automatically invokes the right constructors/destructors on every value change — it works with any type, including classes with non-trivial special members, without a single manual placement-new operation on the user's side.

Как это работает под капотомHow it works under the hood

std::visit — основной идиоматичный способ работать с variant без ручных if/switch по index(). Он вызывает переданный callable с активным значением, требуя, чтобы callable умел принять любой из типов variant. Частый идиом — overloaded: класс, наследующий сразу от нескольких лямбд и объединяющий их operator() через using, чтобы получить один объект, перегруженный по каждому типу.std::visit is the main idiomatic way to work with a variant without manual if/switch on index(). It calls the given callable with the active value, requiring the callable to accept any of the variant's types. A common idiom is overloaded: a class that inherits from several lambdas at once and merges their operator() via using, producing a single object overloaded per type.

// idiom "overloaded" — объединение нескольких лямбд в один visitor// the "overloaded" idiom — merging several lambdas into one visitor
template <class... Ts>
struct overloaded : Ts... { using Ts::operator()...; };

template <class... Ts>
overloaded(Ts...) -> overloaded<Ts...>;  // deduction guide (см. CTAD)// deduction guide (see CTAD)

std::variant<int, double, std::string> v = "hi";

std::visit(overloaded{
    [](int i)             { std::cout << "int: " << i; },
    [](double d)          { std::cout << "double: " << d; },
    [](const std::string& s) { std::cout << "string: " << s; }
}, v);  // вызовется третья лямбда — активен string// the third lambda runs — string is active

Все ветки visitor обязаны возвращать один и тот же тип (или все — void) — компилятор должен статически знать единственный тип результата std::visit, вне зависимости от того, какая ветка реально выполнится в рантайме. Альтернатива нескольким лямбдам — одна generic-лямбда с if constexpr внутри, проверяющая тип через if constexpr (std::is_same_v<T, int>) и так далее.Every branch of the visitor must return the same type (or all must return void) — the compiler needs to statically know a single result type for std::visit, regardless of which branch actually executes at runtime. An alternative to multiple lambdas is a single generic lambda with if constexpr inside, checking the type via if constexpr (std::is_same_v<T, int>) and so on.

ГраблиGotchas

variant не может быть «пустым» намеренно — для этого нужен optionalvariant can't be intentionally "empty" — that's what optional is for

У variant есть валидное состояние "no value" (valueless_by_exception() возвращает true, index() равен variant_npos), но оно возникает только как побочный эффект исключения, брошенного во время смены активного значения (редкий edge-кейс, обычно связанный с throw в конструкторе при присваивании) — это не способ намеренно создать «пустой» variant. Если задача — «может присутствовать значение одного из типов, а может не быть значения вовсе», это optional<variant<...>> или просто optional<T>, не голый variant.variant does have a valid "no value" state (valueless_by_exception() returns true, index() equals variant_npos), but it only arises as a side effect of an exception thrown during a change of the active value (a rare edge case, usually tied to a throw inside a constructor during assignment) — it's not a way to deliberately create an "empty" variant. If the task is "there might be a value of one of these types, or there might be no value at all," that's optional<variant<...>> or simply optional<T>, not a bare variant.

std::visit требует одного типа возврата у всех ветокstd::visit requires a single return type across all branches

Если одна лямбда в overloaded возвращает int, а другая — std::string, это ошибка компиляции, а не выбор типа в рантайме — std::visit должен вывести единственный статический тип результата. Решение — либо привести все ветки к общему типу (например, обернуть в variant или optional результата), либо использовать одну generic-лямбду с if constexpr для ветвления внутри одного тела.If one lambda in overloaded returns int and another returns std::string, that's a compile error, not a runtime type choice — std::visit must deduce a single static result type. The fix is either to unify all branches to a common type (e.g., wrap the result in a variant or optional), or to use one generic lambda with if constexpr branching inside a single body.

Набор типов фиксирован на этапе компиляции — это не гибкость std::anyThe set of types is fixed at compile time — this isn't std::any's flexibility

variant подходит, только если все возможные альтернативы известны заранее и перечислены в шаблонных параметрах. Если набор типов формируется динамически или неизвестен на этапе компиляции (плагины, рантайм-конфиги произвольной формы), variant не годится — нужен std::any, с соответствующей потерей части типобезопасности при извлечении.variant only fits when all possible alternatives are known ahead of time and listed as template parameters. If the set of types is formed dynamically or is unknown at compile time (plugins, arbitrarily-shaped runtime configs), variant doesn't fit — you need std::any, with a corresponding loss of some type safety on extraction.