C++ 17 中的 std::variant 与 std::any:区别、使用场景与实践指南

在 C++ 17 之前,处理不确定类型或需要类型安全的容器时,程序员常常使用 boost::variantboost::any 或手写类型擦除。随着标准库的扩展,C++ 17 引入了 std::variantstd::any,为类型安全和灵活性提供了标准化解决方案。本文将系统剖析它们的区别、适用场景,并给出实战代码示例,帮助你在项目中更高效地使用这两种类型擦除容器。

1. 语义对比

特性 std::variant std::any
类型安全 静态类型安全:编译期已知可能类型,访问时使用 std::getstd::visit,避免运行时错误 动态类型安全:运行时检查类型,使用 any_cast,若类型不匹配抛出异常
存储大小 大小为 sizeof(max(sizeof(Ti))) + sizeof(alignment),需要所有可能类型已知且固定 需要额外的 type_info 以及内存分配,存储更大
构造/复制 只能对已知类型进行构造,复制时使用对应类型的拷贝构造 需要 typeid 判断,复制时会调用对应类型的拷贝构造
性能 访问性能更好,visit 采用模式匹配 访问时涉及 typeid 对比,略慢
用途 用于实现“多态但有限种类”的数据结构,例如 AST 节点、配置项 用于“任意类型”或类型未知的容器,例如插件系统、通用事件回调

2. 典型使用场景

2.1 std::variant

  • 表达式树:不同类型的节点(如整数、浮点、变量、运算符)可用 variant 包装,访问时使用 visit
  • 配置文件解析:键值对可能是字符串、整数、布尔值等,使用 variant 保存统一容器。
  • 错误码与数据:函数返回值可能是成功(带数据)或错误(带错误码),可使用 variant 表示。

2.2 std::any

  • 插件系统:插件提供的接口参数类型不确定,使用 any 进行统一包装。
  • 事件总线:不同事件携带不同数据结构,统一存入 any 通过 any_cast 解析。
  • 动态属性:对象属性值类型不确定,使用 any 存储。

3. 实战代码

3.1 用 variant 实现简单算术表达式树

#include <iostream>
#include <variant>
#include <vector>
#include <memory>

struct Expr;

using ExprPtr = std::shared_ptr <Expr>;
using ExprNode = std::variant<
    int,                     // 常数
    std::string,             // 变量
    std::vector <ExprPtr>>;   // 递归表达式(仅演示)

struct Expr {
    ExprNode node;
};

int evaluate(const ExprPtr& expr, const std::unordered_map<std::string, int>& env) {
    return std::visit([&](auto&& arg) -> int {
        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, std::string>)
            return env.at(arg);
        else if constexpr (std::is_same_v<T, std::vector<ExprPtr>>)
            // 简单示例:假设是二元加法
            return evaluate(arg[0], env) + evaluate(arg[1], env);
    }, expr->node);
}

int main() {
    auto two = std::make_shared <Expr>(Expr{2});
    auto x   = std::make_shared <Expr>(Expr{"x"});
    auto add = std::make_shared <Expr>(Expr{std::vector<ExprPtr>{two, x}});

    std::unordered_map<std::string, int> env{{"x", 5}};
    std::cout << "2 + x = " << evaluate(add, env) << std::endl; // 输出 7
}

3.2 用 any 做事件总线

#include <iostream>
#include <any>
#include <string>
#include <vector>
#include <functional>

class EventBus {
public:
    using Listener = std::function<void(const std::any&)>;

    void subscribe(const std::string& type, Listener l) {
        listeners[type].push_back(std::move(l));
    }

    void publish(const std::string& type, const std::any& payload) {
        if (auto it = listeners.find(type); it != listeners.end()) {
            for (auto& l : it->second) l(payload);
        }
    }

private:
    std::unordered_map<std::string, std::vector<Listener>> listeners;
};

struct MouseEvent { int x, y; };
struct KeyEvent  { char key; };

int main() {
    EventBus bus;

    bus.subscribe("mouse", [](const std::any& payload){
        const auto& e = std::any_cast <MouseEvent>(payload);
        std::cout << "Mouse at (" << e.x << ", " << e.y << ")\n";
    });

    bus.subscribe("key", [](const std::any& payload){
        const auto& e = std::any_cast <KeyEvent>(payload);
        std::cout << "Key pressed: " << e.key << '\n';
    });

    bus.publish("mouse", MouseEvent{100, 200});
    bus.publish("key",   KeyEvent{'a'});
}

4. 常见坑与最佳实践

  1. variant 必须知道所有可能类型:若类型过多导致编译慢,可考虑使用 std::variantin_place_index 预先构造。
  2. variant 访问要用 std::visit 而不是 getvisit 允许一次性处理所有变体成员,避免多次 get 带来的重复判断。
  3. any_cast 失败抛异常:在需要容忍类型不匹配的场景,可使用 `any_cast (&any)`,返回指针或 `nullptr`。
  4. 避免过度使用 any:如果能在编译期确定类型,优先使用 variant 或模板,any 主要用于真正的“任意类型”需求。
  5. 内存管理any 可能涉及堆分配;若频繁使用,应考虑自定义池或预分配方案。

5. 结语

std::variantstd::any 为 C++ 开发者提供了更安全、更高效的类型擦除工具。了解它们的语义差异、适用场景和常见坑,可以让你在项目中更恰当地选择。无论是实现表达式树、配置系统还是插件事件总线,掌握这两者都能让代码更简洁、可维护。祝你编码愉快!

发表评论