为什么会纠结 variant 的选择
在 C++17 之前,boost::variant 是处理类型安全联合体的主要工具。C++17 引入 std::variant 后,很多项目面临迁移还是继续使用的选择。下面整理几个关键差异,帮助判断当前项目适合哪一种。
选择 std::variant 还是 boost::variant 主要取决于项目对 Boost 的依赖程度以及编译器的 C++17 支持情况。如果项目已经大量使用 Boost 且迁移成本高,继续使用 boost::variant 是合理的;如果追求标准一致性并减少第三方依赖,应优先采用 std::variant。此外,std::variant 是 C++17 标准库的一部分,而 boost::variant 需要额外安装 Boost 库,在跨平台或团队协作中可能增加构建复杂性。
可以先检查编译器版本,确认是否完整支持 C++17(GCC 7 以上、Clang 5 以上、MSVC 2017 15.3 以上)。如果团队已经全面切到 C++17,可以逐步从 boost::variant 迁移到 std::variant,但需要评估现有代码中 Boost 其他模块的依赖——如果只有 variant 依赖 Boost,那迁移收益很高;如果整个项目重度依赖 Boost 的 asio、filesystem 等,那保留 boost::variant 反而更省事。
迁移时需要注意的接口差异
从 boost::variant 迁移到 std::variant 时,需注意接口差异。boost::variant 支持递归定义(通过 boost::recursive_variant),而 std::variant 本身不支持,需要手动用指针或 std::unique_ptr 包装。访问方式上,std::variant 使用 std::visit 或 std::get,而 boost::variant 提供类似接口但细节不同,例如 boost::get 在类型不匹配时可能返回空指针或抛出异常,而 std::get 统一抛出 std::bad_variant_access。建议在迁移时逐一测试所有访问路径,确保行为一致。
例如,boost::variant 的递归定义常用于 AST 节点:
struct Node; using Variant = boost::variant>; 迁移到 std::variant 需要改为:struct Node; using Variant = std::variant>; 访问时 std::visit 更适合替代原来的 apply_visitor,而 std::get 的异常行为需要格外注意——原来的 boost::get 返回空指针可能被隐式忽略,迁移后必须捕获异常或改用 std::get_if 判断指针。
默认构造与空状态的坑
std::variant 的默认构造函数会构造第一个替代类型,因此如果第一个类型没有默认构造函数,则 variant 无法默认构造。解决方法是使用 std::monostate 作为第一个类型,它可默认构造且不存储额外数据。类似地,boost::variant 也面临此问题,通常用 boost::blank 作为占位符。此外,std::variant 赋值时如果新旧类型不同,会先析构旧值再构造新值,期间可能抛出异常导致 variant 进入无效状态(除 valueless_by_exception 外)。确保替代类型的析构函数不抛异常,或使用 noexcept 操作避免异常传播。
实际踩过的坑:如果一个 variant 含有 std::string 和 int,且 std::string 在第一个位置,那么默认初始化会分配一个空字符串,这在小对象上没什么问题,但如果第一个类型是某个重量级资源类且无默认构造函数,编译就过不去。正确的做法是把 std::monostate 放在第一位:std::variant,这样默认构造得到的是 monostate 值,相当于空状态。赋值抛异常导致 valueless_by_exception 的情况虽然罕见,但可以通过 valueless_by_exception() 检查,并在出现时回退或终止。建议所有类型都保证析构不抛异常,并在赋值前用 if constexpr 检查 noexcept 条件。
性能和内存布局
在典型编译器(GCC、Clang)上,std::variant 和 boost::variant 的访问性能接近,因为两者底层都基于联合体或类型擦除。但 std::variant 作为标准库实现,其内部可能利用 constexpr 支持做少量优化,而 boost::variant 的某些版本可能因历史兼容性保留额外开销。性能差异通常不超过个位数百分比,不值得作为决策依据。真正需要关注的是内存布局:sizeof(std::variant) 通常是 max(sizeof(A), sizeof(B)) 再加上一个整数的索引大小(通常 1 字节但可能因对齐而膨胀)。boost::variant 布局类似,但不同 Boost 版本有细微差异。如果你的替代类型中包含大于 64 字节的结构体,建议用 sizeof 实际打印确认,避免在内存受限区域意外膨胀。另外,std::variant 标准未强制要求无动态内存分配,但典型实现不会分配堆内存;boost::variant 的某些递归实现可能通过类型擦除分配堆内存,需要版本确认。
实用判断清单
- 如果项目已深度绑定 Boost 且无迁移计划,继续用 boost::variant 最稳。
- 如果编译器完全支持 C++17 且新模块开发,优先用 std::variant,减少第三方依赖。
- 迁移时注意递归 variant 需要改用手动指针包装,并测试所有访问路径的异常行为。
- 默认构造问题:用 std::monostate 或 boost::blank 作为第一个类型。
- 赋值异常风险:确保所有替代类型的析构函数 noexcept,并考虑检查 valueless_by_exception。
- 性能敏感时自己写微基准测试,不要依赖网上结论。
- 跨平台构建中,std::variant 无需额外安装 Boost,能省去 cmake 查找 boost 的麻烦。
这些判断基于通用经验,具体环境仍需结合编译器和项目依赖确认。如果遇到边界情况,最简单的做法是写一个小 demo 在目标编译器上验证 sizeof、编译时错误和异常行为,再决定最终方案。