C++17 中的 std::variant 与 std::any 的区别及应用场景

在 C++17 之前,std::any 用于存储任何类型的值,但它在运行时并不知道具体的类型,导致访问时需要显式的类型检查和转换。std::variant 是一种类型安全的多态容器,它在编译时就确定了所有可能的类型,并通过索引或访问器访问相应的值。两者虽然都可以存储“任意”类型,但在使用方式、性能、语义以及安全性上存在显著差异。下面从概念、实现细节、性能、语义以及典型场景四个角度来对比这两种容器,并给出实际代码示例。

1. 概念区别

std::any std::variant
定义 存储任意类型的对象,类型信息通过 RTTI 保存 存储一组预定义类型中的任意一种,类型信息通过编译期元组维护
类型安全 运行时检查(使用 any_cast 编译时类型安全(`std::get
` 必须与声明的类型一致)
存储方式 通过 type-erasure 抽象 通过联合体(union)+ 标记(index)实现
可用性 仅限 C++17 起 仅限 C++17 起

2. 实现细节

std::any

std::any 内部实现类似:

class any {
    struct placeholder {
        virtual ~placeholder() = default;
        virtual placeholder* clone() const = 0;
        virtual const std::type_info& type() const = 0;
    };
    template<typename T>
    struct holder : placeholder {
        T value;
        explicit holder(T&& v) : value(std::forward <T>(v)) {}
        placeholder* clone() const override { return new holder(value); }
        const std::type_info& type() const override { return typeid(T); }
    };
    std::unique_ptr <placeholder> content;
public:
    template<typename T>
    any(T&& v) : content(new holder<std::decay_t<T>>(std::forward<T>(v))) {}
    // ...
};
  • 采用 type-erasure:所有类型共享同一基类,真正的数据保存在派生类 `holder ` 中。
  • 每次拷贝都需要 clone(),实现了深拷贝。

std::variant

实现方式:

template<typename... Ts>
class variant {
    static constexpr std::size_t size_ = sizeof...(Ts);
    std::aligned_union_t<0, Ts...> storage;
    std::size_t index;

    template<std::size_t I, typename T>
    static void destroy_at() { 
        reinterpret_cast<T*>(&storage)->~T(); 
    }
    // ... 访问、赋值等
};
  • 通过 aligned_union_t 预留足够的空间来存放任意类型。
  • index 用于记录当前存储的类型索引。
  • 只需要在构造/赋值时执行一次构造/析构,拷贝时也只拷贝对应类型的对象。

3. 性能比较

std::any std::variant
拷贝 需要虚函数调用 + heap 分配(默认实现) 只做一次构造拷贝,无虚函数
访问 运行时 typeid + dynamic_cast 或 any_cast 编译时索引 + 静态访问,性能更好
内存 需要存储类型信息(std::type_info 指针) 只存储索引(size_t
线程安全 对单个 any 对象的拷贝/赋值不是线程安全的 同上,内部实现同样非线程安全

经验总结:如果你需要经常拷贝、访问,并且类型集合已知且有限,std::variant 更高效;如果类型不确定或需要真正的 “任意类型” 存储(比如动态插件系统),std::any 更合适。

4. 语义差异

  • 类型检查std::variant 在编译期就能判断你请求的类型是否存在,编译错误;std::any 需要在运行时检查,否则会抛出 bad_any_cast
  • 访问方式std::variant 支持 `std::get ()`、`std::visit`、`index()` 等;`std::any` 仅支持 `any_cast`,没有直接的索引或多态访问方式。
  • 异常安全std::variant 的构造/析构遵循 RAII,异常不会导致资源泄漏;std::any 由于使用 virtual base 需要仔细处理析构。

5. 典型应用场景

场景 推荐使用
1. 需要存储多种已知类型的数据结构(如 AST 节点) std::variant
2. 需要统一接口来传递任意用户自定义类型(如事件系统) std::any
3. 实现“自定义属性表”或“元数据容器” 视情况而定;若属性类型多且可预知,则 variant 更合适;若属性来源多变,则 any
4. 需要高性能、频繁访问的多态容器 std::variant
5. 与第三方库交互,要求兼容多种返回值 根据第三方提供的 API 选择

6. 代码示例

6.1 std::variant 的简单用法

#include <variant>
#include <iostream>
#include <string>

int main() {
    std::variant<int, double, std::string> v;

    v = 42;                      // 赋值 int
    std::visit([](auto&& arg) { std::cout << arg << '\n'; }, v);

    v = 3.14;                     // 赋值 double
    std::visit([](auto&& arg) { std::cout << arg << '\n'; }, v);

    v = std::string("hello");     // 赋值 string
    std::visit([](auto&& arg) { std::cout << arg << '\n'; }, v);
}

6.2 std::any 的简单用法

#include <any>
#include <iostream>
#include <string>

int main() {
    std::any a = 10;
    std::cout << std::any_cast<int>(a) << '\n';

    a = std::string("world");
    std::cout << std::any_cast<std::string>(a) << '\n';
}

7. 小结

  • std::variant:类型安全、性能好、适合已知类型集合;编译期确定类型,访问更安全。
  • std::any:类型不确定、实现简单;运行时类型检查,性能略逊,但更灵活。

在实际项目中,建议先评估需求类型是否可预知。如果可以,优先使用 std::variant;否则才考虑 std::any。这两者在 C++17 之后成为标准容器,具备良好的可移植性和成熟的实现。

发表评论