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

在 C++17 中,标准库提供了两种常见的类型擦除容器:std::variantstd::any。它们看起来相似,但在语义、类型安全、性能以及使用场景上存在显著差异。本文将从定义、类型安全、操作方式、性能以及典型使用案例等角度,对二者进行深入比较,并给出实际编码建议。

1. 基本定义

关键字 说明
std::any 允许存放任意类型的值,但在编译期无法获知存放的具体类型。
std::variant 只能存放预先指定的一组类型之一,编译期已知类型集合。

std::any 的实现类似于类型擦除(type erasure),内部使用动态分配存储对象,并记录其完整类型信息;std::variant 则使用 unionstd::variant_alternative 机制,采用位域记录当前值的类型索引。

2. 类型安全

  • std::any:在取值时需要使用 `any_cast ` 指定期望类型,如果实际类型不匹配会抛出 `std::bad_any_cast`。此过程在运行时检查,编译器无法提前捕获错误。
  • std::variant:通过 `std::get ` 或 `std::get_if` 访问值,如果类型不匹配会抛出 `std::bad_variant_access` 或返回 `nullptr`。由于 `variant` 的类型集合已在编译期确定,编译器可以在许多情况下对访问路径进行检查,减少运行时错误。

3. 性能

维度 std::any std::variant
内存布局 需要动态分配(heap)或至少使用 SSO(small string optimization) 只在栈上存储固定大小的内存,避免堆分配
运行时检查 需要完整的 RTTI 以及异常抛掷 仅需要索引比较,异常处理更轻量
适配器 需要 typeidany_cast 的模板匹配 需要 variantvisitget 语义,访问成本更低

综上,若性能是关键因素且类型集合已知,std::variant 更优;若类型未知或需要高度动态的对象存储,std::any 仍有价值。

4. 使用场景

场景 推荐容器
插件系统:对象类型多且未知 std::any 或自定义 type-erased base
事件系统:事件类型固定且多 std::variant
调试信息:存放多种调试对象 std::any
状态机:有限状态机中的状态类 std::variant
数据持久化:序列化不同字段 std::variant 与 visitor
跨语言接口:不确定类型 std::any 或 boost::any

5. 代码示例

5.1 std::any

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

int main() {
    std::any a = 10;          // 存放 int
    a = std::string{"hello"}; // 替换为 string

    try {
        std::cout << std::any_cast<int>(a) << '\n'; // 抛异常
    } catch(const std::bad_any_cast& e) {
        std::cout << "bad_any_cast: " << e.what() << '\n';
    }

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

5.2 std::variant

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

int main() {
    std::variant<int, std::string, std::vector<int>> v = 42;

    // 访问
    std::cout << std::get<int>(v) << '\n';

    // 访问失败
    try {
        std::cout << std::get<std::string>(v) << '\n';
    } catch(const std::bad_variant_access& e) {
        std::cout << "bad_variant_access: " << e.what() << '\n';
    }

    // visitor
    std::visit([](auto&& arg){
        std::cout << "value: " << arg << '\n';
    }, v);

    // 变换为 vector
    v = std::vector <int>{1,2,3};
    std::visit([](auto&& arg){
        std::cout << "vector size: " << arg.size() << '\n';
    }, v);
}

6. 与 boost::variant / std::any 的对比

  • boost::variantstd::variant 功能相近,但 boost::variant 在 C++11 时就出现,支持更旧的编译器。std::variant 在性能与标准兼容性方面更好。
  • boost::anystd::any 同理。若项目已使用 Boost,可根据需求保留或迁移。

7. 进阶技巧

  1. 自定义 visitor:利用 std::variantvisit 可以轻松实现多态处理。
  2. 默认值std::variant 可以在构造时指定默认类型,使用 std::variant<T...> v; 时默认值是第一个类型的默认构造。
  3. std::monostate:可作为占位符,让 variant 在空状态下返回默认值。
  4. 异常安全variantemplaceoperator= 在强异常安全保证下完成。

8. 小结

  • std::any:适用于需要动态、类型未知存储的场景;提供最少的类型信息,使用时需手动检查类型并处理异常。
  • std::variant:适用于类型集合已知、需要高性能或类型安全的场景;编译期已确定类型,访问更安全、效率更高。

在实际项目中,常见的做法是将 std::variant 用于内部实现(例如事件或状态机),而将 std::any 用于插件接口或外部 API 的参数传递。合理选择与组合,可让 C++ 代码既灵活又高效。

发表评论