如何使用 C++17 的 std::variant 进行类型安全的多态处理

在 C++17 标准中,std::variant 被引入来解决传统多态(如 void*std::any、以及类继承层次结构)所带来的类型不安全与性能损失。本文将详细介绍 std::variant 的基本使用、访问方式、遍历访问、与其他 STL 容器结合使用,以及如何借助 std::visit 实现类型安全的多态函数。


1. std::variant 简介

std::variant 是一个可存放多种类型的容器,只能存放 一种 类型的值。它相当于一个“类型安全的” std::union,但提供了更强的类型检查和更丰富的操作接口。

std::variant<int, double, std::string> var;
var = 42;                  // 存储 int
var = 3.14;                // 覆盖为 double
var = std::string("hello"); // 覆盖为 std::string

若想存放自定义类型,必须确保该类型满足 std::variant 的要求:可移动、可拷贝、可比较(可选)等。


2. 访问和检查当前存储类型

2.1 std::get / std::get_if

int i = std::get <int>(var);            // 如果 var 当前不是 int,会抛 std::bad_variant_access
int* p = std::get_if <int>(&var);       // 若是 int,返回指针,否则返回 nullptr

2.2 std::holds_alternative

if (std::holds_alternative <double>(var)) {
    // ...
}

2.3 index()type()

size_t idx = var.index();                 // 当前类型的索引,0 表示第一个类型
const std::type_info& ti = var.type();    // 当前类型的 type_info

3. std::visit:访问者模式的现代实现

std::visit 接受一个可调用对象(如 lambda、函数对象、函数指针)和一个或多个 variant,根据 variant 当前存储的类型调用对应的重载版本。

auto visitor = [](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, double>) {
        std::cout << "double: " << arg << '\n';
    } else {
        std::cout << "string: " << arg << '\n';
    }
};

std::visit(visitor, var);

如果传入多个 variant,std::visit 会使用 参数包展开 并生成对应组合的重载。

std::variant<int, std::string> a = 10;
std::variant<double, std::string> b = "test";

std::visit([](auto&& va, auto&& vb) {
    std::cout << va << " | " << vb << '\n';
}, a, b);

4. 与其他 STL 容器结合

4.1 std::vector<std::variant<...>>

std::vector<std::variant<int, std::string>> vec;
vec.push_back(42);
vec.push_back("hello");

遍历时可使用 std::visitstd::get_if

4.2 递归结构

自定义递归类型时,需要使用 std::variant 包装指针或 std::shared_ptr

struct Expr;
using ExprPtr = std::shared_ptr <Expr>;

struct Const { int value; };
struct Add { ExprPtr left, right; };

struct Expr : std::variant<Const, Add> {
    using variant::variant;
};

这样可以避免无限递归类型定义。


5. 性能考虑

  • std::variant 的内部实现类似于 union + index。访问时不需要虚函数表,避免了动态分派的开销。
  • std::visit 的调用会被编译器优化为 switchif-else,并可使用模板递归展开,生成高效代码。
  • std::any 相比,std::variant 的类型检查是 编译时 的,能在编译期捕获错误。

6. 常见陷阱

  1. 缺失类型
    若在 variant 中未包含所需的类型,编译器会报错。

    std::variant <int> v;
    v = 3.14; // 错误
  2. 未捕获 bad_variant_access
    使用 std::get 访问错误类型会抛异常。

    try { std::get <double>(v); }
    catch (const std::bad_variant_access& e) { /* 处理 */ }
  3. 递归结构导致堆栈溢出
    如前所述,需要使用指针包装递归类型。


7. 典型应用场景

场景 传统实现 std::variant 实现 优点
消息系统 基于 enum + union + 手动 switch variant + visit 更安全、易扩展
解析树 多个类继承自基类 递归 variant 消除虚表,降低内存占用
配置文件 std::map<std::string, boost::variant> std::variant 省去第三方库,标准化
UI 事件 std::variant std::variant 统一事件类型,避免类型转换

8. 小结

std::variant 为 C++ 提供了 类型安全的代替 union,与 std::visit 的配合实现了 现代多态 的高效方案。相比传统的 void*std::any 或继承层次结构,variant 在类型检查、性能和易用性上都有显著提升。通过本文的示例与技巧,相信读者已经具备使用 std::variant 进行安全多态编程的基本能力。


发表评论