C++中的std::variant有什么作用?(类型安全的联合体union替代品)

std::variant 解决传统 union 类型不安全问题,通过内置类型标签实现安全访问;支持 std::get、std::get_if、std::holds_alternative 安全读写,并借助 std::visit 实现类型匹配分发。

std::variant 能解决什么问题

传统 union 允许在一块内存里存不同类型的值,但不记录当前实际存的是哪种类型,访问错误类型会直接导致未定义行为。比如你写了 union { int i; double d; },然后往里面写了 i = 42,却读 d,程序就崩了——编译器不管,运行时也不报错,只等出事。

std::variant 把类型信息“带在身上”,每次赋值或修改都会更新内部的类型标签,读取前还能用 std::holds_alternativestd::get_if 安全检查,从根本上堵住误读漏洞。

怎么安全地读写 std::variant 的值

不能像 union 那样直接点成员或强制转型。必须通过标准接口操作,否则编译失败(这是好事)。

  • 写入:直接赋值,例如 v = 42v = 3.14v = std::string{"hello"}v 会自动切换到对应类型
  • 读取单个类型:用 std::get(v) —— 如果当前不是 T 类型,抛 std::bad_variant_access
  • 安全读取:先用 std::get_if(&v) 拿指针,返回 nullptr 表示当前不是 T,否则解引用访问
  • 类型判断:用 std::holds_alternative(v) 得到 bool
std::variant v = "abc";
if (std::holds_alternative(v)) {
    std::cout << std::get(v); // ok
} else {
    std::cout << std::get(v); // 不会执行
}

std::visit 是怎么配合 variant 一起用的

当你要对多种可能类型做不同处理(比如打印、序列化、计算),逐个 if/else + holds_alternative 写起来啰嗦还容易漏分支。std::visit 提供了一种类型安全的“分发”机制,它会在运行时根据 variant 当前实际类型,自动调用匹配的重载函数。

  • 参数必须是可调用对象(lambda、函数对象、普通函数),且每个重载都得覆盖 variant 所有可选项
  • 如果漏掉某个类型,编译失败,而不是运行时报错
  • 支持多个 variant 同时 visit(C++17 起),适合组合场景
std::variant v = 3.14;
std::visit([](const auto& x) {
    using T = std::decay_t;
    if constexpr (std::is_same_v) {
        std::cout << "int: " << x;
    } else if constexpr (std::is_same_v) {
        std::cout << "double: " << x;
    } else if constexpr (std::is_same_v) {
        std::cout << "string: " << x;
    }
}, v);

要注意的边界和开销

std::variant 不是零成本抽象。它比裸 union 多占至少一个字节(用于存储类型索引),而且构造/赋值/析构都涉及分支判断和可能的内存操作。

  • 如果所有备选类型都是 trivial(如 intfloat),variant 本身也是 trivial 的;但只要有一个非 trivial 类型(比如 std::string),它就会带上默认构造、析构等逻辑
  • 不能存引用、数组、void、抽象类等无法实例化的类型
  • 不能包含自身(std::variant> 不合法),需要用 std::unique_ptr 或间接方式绕过
  • 移动语义正常工作,但注意某些类型(如 std::string)移动后状态不确定,别在 visit 里反复用同一个变量多次访问

真正关键的是:别把它当成“万能容器”滥用。如果类型组合固定、逻辑清晰,variant 是极好的选择;但如果类型太多、嵌套太深、或者需要动态增删类型,该用多态或其它设计模式就别硬扛。