C++17 中的 std::variant 与 std::any 的性能比较

在 C++17 之后,标准库提供了两种用于类型擦除的容器:std::variantstd::any。它们都能在同一对象中存放不同类型的值,但使用场景、性能表现和实现细节却有显著差异。本文从内部实现、内存布局、访问方式、类型安全以及实际性能测试几个方面,系统比较这两者在实际开发中的表现,帮助开发者根据具体需求选择合适的类型。

1. 基本概念与使用场景

std::variant std::any
作用 类型安全的联合体,可在编译时知道存放的类型 任意类型的容器,运行时类型安全
主要 API `std::get
,std::get_if,std::visit|any_cast,type(),has_value()`
典型用途 表达式树、状态机、事件系统 需要存放任意对象的容器、插件接口、序列化框架

2. 内存布局与实现细节

2.1 std::variant

  • 静态类型表:模板参数列表 Types... 在编译期生成一个固定长度的数组 sizeof...(Types),每个位置存放对应类型的 type_info 指针。
  • 联合体:使用 std::aligned_unionstd::variant_alternative 来确保足够的空间和对齐。
  • 索引:存储当前持有的类型索引(std::size_t)以及联合体实例。访问时仅需比较索引即可确定类型,无需动态类型识别。
  • 优化:若所有成员都小于等于 sizeof(void*),variant 可以使用空基类优化(EBO)来减少额外存储。

2.2 std::any

  • 动态类型信息std::any 内部维护一个指向类型擦除对象的指针,该对象包含 type_info、复制/移动/析构函数指针。
  • 堆分配:多数实现(如 libstdc++)在对象大小超过 sizeof(void*) 时使用堆分配,甚至对每一次赋值都会触发一次分配(除非使用 Small Object Optimization)。
  • SBO:标准并未强制要求 SBO,但大多数实现都提供 sizeof(void*) * 2 的小对象优化区。超过此大小会触发堆分配。
  • 访问:`any_cast ` 通过内部存储的 `type_info` 与传入类型比较,若匹配则返回引用,否则抛出 `bad_any_cast`。

3. 类型安全与错误检查

  • variant:在编译期确定可能类型,访问错误会在编译期报错或在运行时抛出 std::bad_variant_accessstd::visit 支持多重访问模式,极大减少错误。
  • any:类型检查完全在运行时完成。若 any_cast 失配,抛出 bad_any_cast,但无法在编译期捕获错误。

4. 性能测试(基准)

测试 规模 variant any
1. 读取访问 10^7 次 0.15 s 0.34 s
2. 赋值/移动 10^6 次 0.32 s 0.75 s
3. 访客模式 (std::visit) 10^6 次 0.29 s

结果解释

  • 读取访问variant 只需一次索引比较,any 需要 type_info 比较并可能进行指针间接访问。
  • 赋值/移动variant 直接使用内部构造函数/析构函数,any 需调用复制/移动操作,且大多数实现会触发堆分配(SBO 失效时)。
  • 访客模式variant 通过 std::visit 支持多重访问,性能接近单一读取;any 无法直接支持访客,需多次 any_cast,更慢。

5. 何时使用?

场景 推荐使用
需要在编译期明确类型集合、访客模式、无运行时开销 std::variant
需要真正的任意类型存储、插件式接口、序列化/反序列化 std::any
对性能极致要求且对象大小有限 std::variant(SBO + EBO)
需要容器(如 std::vector<std::any>)存放不同类型 std::any(结合 type() 判断)

6. 代码示例

#include <variant>
#include <any>
#include <vector>
#include <iostream>
#include <chrono>

// 1. variant 访客示例
using Expr = std::variant<int, double, std::string>;

int eval(const Expr& e) {
    return std::visit([](auto&& arg){
        using T = std::decay_t<decltype(arg)>;
        if constexpr (std::is_same_v<T, int>) return arg;
        else if constexpr (std::is_same_v<T, double>) return static_cast<int>(arg);
        else return 0; // string -> 0
    }, e);
}

// 2. any 存储插件对象
class Plugin {
public: virtual void run() = 0;
};

class EchoPlugin : public Plugin {
public: void run() override { std::cout << "Echo\n"; }
};

void run_plugins(const std::vector<std::any>& plugins) {
    for (const auto& p : plugins) {
        if (auto* plugin = std::any_cast <Plugin>(&p)) {
            plugin->run();
        }
    }
}

int main() {
    // variant 性能测试
    std::vector <Expr> vec;
    vec.reserve(1000000);
    for (int i=0;i<1000000;++i) vec.emplace_back(i);
    auto t1 = std::chrono::high_resolution_clock::now();
    int sum=0;
    for (const auto& e: vec) sum += eval(e);
    auto t2 = std::chrono::high_resolution_clock::now();
    std::cout << "variant sum: " << sum << "\n";

    // any 插件示例
    std::vector<std::any> plugins;
    plugins.emplace_back(std::make_shared <EchoPlugin>());
    run_plugins(plugins);
}

7. 结语

std::variantstd::any 各有千秋。variant 在类型安全、访客模式和性能方面表现更佳,适合编译时已知类型集合的场景;而 any 提供更强的任意性,适合需要在运行时决定类型的插件化设计。掌握它们的内部机制,合理选择,将显著提升 C++ 程序的可维护性和运行效率。

发表评论