Введение
Классический union в C++ позволяет хранить в одной области памяти значения разных типов, но он совершенно не типобезопасен: компилятор не следит за тем, какой тип «активен» в данный момент, и не вызывает деструкторы нетривиальных типов автоматически. std::variant, появившийся в C++17, решает эту проблему: он хранит ровно одно значение из заданного набора типов и точно знает, какой тип активен прямо сейчас.
Концепция
std::variant<T1, T2, ..., Tn> — это размеченное объединение (tagged union). В каждый момент времени переменная хранит значение ровно одного из перечисленных типов и «помнит», какой именно. Доступ к значению осуществляется безопасно:
std::get<T>(v)илиstd::get<Index>(v)— бросаетstd::bad_variant_access, если активен другой тип;std::get_if<T>(&v)— возвращает указатель илиnullptr;std::holds_alternative<T>(v)— проверка активного типа;std::visit(visitor, v)— применение visitor-функции/объекта к текущему значению, независимо от того, какой тип активен.
std::visit — ключевая фича: она заставляет обработать все варианты типов (компилятор проверит полноту, если visitor — это перегруженный набор лямбд), что устраняет целый класс ошибок «забыли обработать случай».
Пример кода
#include <variant>
#include <iostream>
#include <string>
#include <vector>
// Набор типов команд, которые может содержать "сообщение"
struct MoveCmd { int dx, dy; };
struct RenameCmd { std::string newName; };
struct DeleteCmd {};
using Command = std::variant<MoveCmd, RenameCmd, DeleteCmd>;
// Вспомогательный шаблон для построения visitor из набора лямбд
template <class... Ts>
struct Overload : Ts... {
using Ts::operator()...;
};
template <class... Ts>
Overload(Ts...) -> Overload<Ts...>;
void execute(const Command& cmd) {
std::visit(Overload{
[](const MoveCmd& m) {
std::cout << "Move by (" << m.dx << ", " << m.dy << ")n";
},
[](const RenameCmd& r) {
std::cout << "Rename to '" << r.newName << "'n";
},
[](const DeleteCmd&) {
std::cout << "Delete objectn";
}
}, cmd);
}
int main() {
std::vector<Command> commands = {
MoveCmd{10, -5},
RenameCmd{"new_name.txt"},
DeleteCmd{}
};
for (const auto& cmd : commands) {
execute(cmd);
}
// Безопасный доступ через holds_alternative / get_if
Command c = MoveCmd{1, 2};
if (std::holds_alternative<MoveCmd>(c)) {
const auto* move = std::get_if<MoveCmd>(&c);
std::cout << "dx = " << move->dx << "n";
}
// std::get бросает исключение, если тип не совпадает
try {
std::get<RenameCmd>(c);
} catch (const std::bad_variant_access& e) {
std::cout << "Caught: " << e.what() << "n";
}
return 0;
}
Пояснения к коду
Command — это вариант из трёх структур-команд. Идиома Overload объединяет несколько лямбд в один объект с перегруженным operator(), что превращает std::visit в аналог switch/match из языков с поддержкой алгебраических типов данных. Если забыть обработать один из вариантов (например, удалить лямбду для DeleteCmd), компилятор выдаст ошибку — это огромное преимущество над union + ручным switch по тегу, где такая ошибка осталась бы незамеченной до выполнения.
std::get_if возвращает указатель, что удобно для проверки «может быть, не сработает» без исключений, а std::get<T> — для случаев, когда мы уверены в типе и хотим явный сигнал об ошибке через исключение.
Подводные камни и советы
1. std::variant не может быть пустым по умолчанию в обычном смысле — он либо хранит значение первого типа (если тот допускает конструктор по умолчанию), либо переходит в специальное состояние valueless_by_exception при неудачном emplace. Это редкий, но не нулевой случай, который нужно учитывать в высоконагруженном коде. 2. Размер variant равен размеру самого большого типа + небольшой оверхед на индекс. Если типы сильно различаются по размеру, рассмотрите хранение указателей/unique_ptr внутри вариантов. 3. std::visit требует, чтобы visitor мог обработать все типы — если типов много и логика разная, удобнее завести виртуальные функции через полиморфизм, а variant оставить для небольшого закрытого набора альтернатив (closed set). 4. Используйте variant для представления «одно из нескольких» в парсерах, конечных автоматах, AST-узлах — везде, где раньше тянулся union + enum-тег или иерархия классов только для диспетчеризации.
================================================================================