C++17 中的 std::variant 与 std::any:区别、适用场景与使用技巧

在 C++17 标准中,std::variant 和 std::any 两个新容器极大地方便了类型安全和类型擦除的实现。它们虽然都能在运行时容纳不同类型的值,但本质上设计目标截然不同。本文将从定义、使用方式、性能、错误处理以及实际应用案例四个维度深入剖析两者,帮助开发者根据具体需求做出最合适的选择。


1. 基本定义与核心语义

std::variant std::any
语义 类型安全的和(sum type) 类型擦除(任何类型的盒子)
编译期类型 在模板参数列表中显式列出 在编译期不知晓,运行时才确定
类型安全 通过 `std::holds_alternative
std::getstd::visit检测/访问 | 通过any_cast` 进行类型检查,若不匹配抛出异常
主要用途 需要在有限种类型中安全切换、模式匹配 需要在运行时存放任意类型、实现泛型接口或消息总线

核心区别variant 是一个“有限的”多态容器,所有可能的类型必须在编译时确定;any 则是一个“一般的”类型擦除容器,能容纳任何类型,甚至是非标准类型。


2. 典型使用场景对比

2.1 std::variant

  1. 配置或选项:例如 std::variant<int, double, std::string> 可以表示用户输入的数值或字符串。
  2. 状态机:状态树中的节点可通过 variant 保存不同形态的数据结构。
  3. 网络消息:对不同协议的数据包使用 variant,配合 std::visit 实现多态处理。
  4. 返回值包装:像 std::optionalstd::expected(C++23)一样,将错误码、成功值或失败信息包装在一个 variant 中。

2.2 std::any

  1. 插件/事件系统:事件总线传递任意类型的数据,监听者通过 any_cast 解析。
  2. 动态属性:对象拥有可动态添加/修改的属性,每个属性可以是任意类型。
  3. 跨库接口:当两个库彼此不共享类型定义时,可以通过 any 传递数据。
  4. 泛型容器:实现一个“多类型”容器,内部使用 std::any 存储不同类型的元素。

3. 性能与资源管理

3.1 结构体大小

  • variant:其大小为 max(sizeof(T1), sizeof(T2), …) + sizeof(size_t),所有成员共用同一块内存空间。
  • any:内部一般为 sizeof(void*) * 2 + sizeof(size_t),包括指针、类型信息等;若对象较大会使用堆分配。

3.2 拷贝与移动

  • variant:复制和移动是值语义,按成员类型的拷贝/移动构造实现。
  • any:复制会调用内部 clone(),若对象是 POD 直接拷贝,否则需要 heap 分配;移动则通常是浅拷贝,保持指针不变。

3.3 异常安全

  • variant:在赋值过程中如果内部构造失败,variant 会保持旧状态。
  • anyany_cast 若类型不匹配会抛出 bad_any_cast,不影响存储的对象。

4. 常见陷阱与最佳实践

场景 说明 解决方案
variant 中的类型重复 std::variant<int, int> 编译错误 移除重复类型
any_cast 失败 忘记检查类型 `any_cast
(&any_obj)或使用any_cast` 后捕获异常
大对象复制 variant 复制大对象时效率低 采用 `std::shared_ptr
std::unique_ptr` 包装
多线程共享 any 对象在多线程环境下未加锁 通过 std::mutexstd::atomic<std::shared_ptr<any>> 进行同步

4.1 代码示例:状态机

using State = std::variant<Idle, Running, Paused, Error>;

void process(State& s) {
    std::visit([](auto&& state){
        using T = std::decay_t<decltype(state)>;
        if constexpr (std::is_same_v<T, Idle>)      { /* 处理 Idle */ }
        else if constexpr (std::is_same_v<T, Running>) { /* 处理 Running */ }
        else if constexpr (std::is_same_v<T, Paused>)   { /* 处理 Paused */ }
        else if constexpr (std::is_same_v<T, Error>)    { /* 处理 Error */ }
    }, s);
}

4.2 代码示例:事件总线

class EventBus {
    std::unordered_map<std::string, std::vector<std::function<void(std::any)>>> subs_;
public:
    template<typename T>
    void subscribe(const std::string& topic, std::function<void(const T&)> cb) {
        subs_[topic].push_back([cb = std::move(cb)](std::any a) {
            if (auto ptr = std::any_cast <T>(&a))
                cb(*ptr);
        });
    }

    template<typename T>
    void publish(const std::string& topic, const T& data) {
        if (auto it = subs_.find(topic); it != subs_.end()) {
            std::any a = data;
            for (auto& f : it->second)
                f(std::move(a));
        }
    }
};

5. 结语

  • 当你需要 受限类型集合编译时类型安全高性能 时,选择 std::variant
  • 当你需要 任意类型、动态扩展跨模块数据传递 时,选择 std::any

二者并不互斥,实际项目中往往会在同一个代码库里同时使用。掌握它们的语义、性能特性以及正确的使用模式,是现代 C++ 开发者提升代码质量与可维护性的关键之一。

发表评论