C++ 中的 std::variant 与 std::any 的区别与适用场景

在 C++17 之后,标准库新增了两种在运行时实现类型安全的容器——std::variantstd::any。它们都可以用来存放不同类型的值,但在使用场景、类型安全性、性能以及语义上存在显著差异。本文从概念、类型安全、异常安全、使用方法、性能比较以及实际应用场景等方面,对两者进行系统对比,并给出在不同业务需求中如何选择的建议。


1. 概念对照

项目 std::variant std::any
定义 一种可变的、静态类型安全的多态容器;其模板参数列表 Variant<Types...> 预先声明所有可能的类型 一种动态类型安全的容器;可以存放任意类型,类型在运行时决定
类型检查 编译期检查,使用 `std::holds_alternative
()std::get()等 | 运行时检查,使用typeidany_cast()`
内存分配 只在内部使用固定大小的缓冲区(max(sizeof(T1), sizeof(T2), …)),不产生堆分配(除非类型自身需要堆) 采用内部 std::any::holder 对象,通常会分配堆内存来保存存储的数据
析构 通过 variant 的类型信息在销毁时调用正确的析构函数 同样通过 any 的类型信息调用析构,但在堆上分配的对象需要额外的堆析构

2. 类型安全与可读性

2.1 std::variant:编译期安全

  • 类型已知:编译器在编译时就知道 variant 能容纳哪些类型,所有访问都在编译期校验。
  • 错误提示更友好:如果你尝试访问一个不在模板参数列表中的类型,编译器会给出错误,避免运行时崩溃。
  • 可读性更高:代码结构类似枚举,读者能一眼看出变量可能的类型。

2.2 std::any:运行时安全

  • 灵活性更高:容器可以在任何时间存放任何类型,适用于不确定类型序列化、插件系统、事件总线等场景。
  • 错误隐蔽:如果你 `any_cast ()` 的类型不匹配,默认抛出 `std::bad_any_cast`,但如果你忽略异常或使用 `any_cast(&any)` 返回指针,会得到 `nullptr`,这在业务逻辑中可能被误认为是合法的空值。

3. 性能对比

指标 std::variant std::any
内存占用 固定大小,取最大类型大小(再加对齐填充) 需要堆内存,且存储对象大小与实际对象大小相同
访问成本 `std::get
()只做索引和偏移 |any_cast()` 需要检查类型、动态分配或拷贝
拷贝/移动 直接调用对应类型的拷贝/移动构造 需要通过类型擦除机制进行构造,可能更耗时
编译时间 对模板参数数量敏感,太多类型会导致编译慢 对类型的依赖更弱,编译时间相对稳定

总结:在高性能、频繁访问的场景,variant 更有优势;在类型不确定或需要动态扩展的业务,any 更合适。


4. 典型使用案例

4.1 std::variant

  1. 实现有限状态机

    struct Loading {};
    struct Success { int data; };
    struct Error { std::string msg; };
    
    using State = std::variant<Loading, Success, Error>;
    State s = Loading{};
  2. 多态函数返回值
    std::variant<int, std::string> parse(const std::string& s) {
        try {
            return std::stoi(s);
        } catch (...) {
            return s;
        }
    }
  3. 树形结构(如 JSON)
    using JsonValue = std::variant<std::nullptr_t, bool, int, double, std::string,
                                   std::vector <JsonValue>, std::unordered_map<std::string, JsonValue>>;

4.2 std::any

  1. 插件系统的参数容器
    std::unordered_map<std::string, std::any> settings;
    settings["threshold"] = 0.85f;
    settings["mode"] = std::string("fast");
  2. 事件总线(Event Bus)
    void dispatch(std::any payload) {
        if (auto p = std::any_cast <int>(&payload)) {
            handleInt(*p);
        } else if (auto p = std::any_cast<std::string>(&payload)) {
            handleString(*p);
        } else {
            handleUnknown(payload);
        }
    }
  3. 通用缓存
    std::unordered_map<std::string, std::any> cache;
    cache["user"] = User{...};
    cache["config"] = Config{...};

5. 何时选择哪一个?

需求 推荐容器
类型范围已知且固定 std::variant
需要在编译期对类型做判定 std::variant
需要高性能、频繁访问 std::variant
类型不确定、可在运行时动态添加 std::any
与反射、序列化框架集成 std::any
需要跨模块传递“任意”类型的数据 std::any
需要保证在运行时不出现类型错误(如插件调用) std::any(配合异常捕获)

6. 代码小技巧

6.1 std::variant 的访问

// 访问多重可能类型
std::visit([](auto&& arg){
    using T = std::decay_t<decltype(arg)>;
    if constexpr (std::is_same_v<T, Success>) {
        std::cout << "Success: " << arg.data << '\n';
    } else if constexpr (std::is_same_v<T, Error>) {
        std::cout << "Error: " << arg.msg << '\n';
    }
}, state);

6.2 std::any 的安全检查

if (auto p = std::any_cast <int>(&anyObj)) {
    std::cout << "int: " << *p << '\n';
} else {
    std::cout << "not int\n";
}

6.3 性能微调

  • std::variant,在模板参数列表中使用 std::monostate 作为空状态,避免不必要的构造成本。
  • std::any,在需要大量拷贝时,可以考虑使用 any_cast<std::reference_wrapper<T>> 来避免复制。

7. 小结

  • std::variant:编译期类型安全、性能优秀、适用于已知有限类型集合的场景。
  • std::any:运行时类型安全、极高的灵活性、适用于类型不确定或需要动态扩展的业务。

了解并灵活运用这两者,能让 C++ 代码在类型安全与灵活性之间找到最佳平衡点。希望本文能帮助你在项目中更好地决定使用哪种容器。

发表评论