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

在 C++17 标准中,为了解决类型安全多态值存储的问题,标准库提供了两种主要的工具:std::variantstd::any。它们看似相似,但在设计理念、使用场景、类型安全以及性能上有显著差异。本文将通过对比两者的特点、典型应用以及实际代码示例,帮助你在项目中做出更合适的选择。

1. 基本概念

  • std::variant<T...>

    • 采用类型擦除变体实现,内部维护一个固定的类型集合。
    • 只能存放预先声明的类型之一,访问时必须使用 std::getstd::visitstd::holds_alternative
    • 访问时是编译期确定的类型,编译器可进行类型检查,避免运行时错误。
  • std::any

    • 采用类型擦除实现,内部可以存放任何非引用的类型。
    • 访问时需要使用 `std::any_cast `,类型必须在运行时匹配,否则抛出 `std::bad_any_cast`。
    • 更像一个“通用容器”,但缺乏编译期安全。

2. 性能对比

特性 std::variant std::any
内存布局 固定大小(对齐后最大类型尺寸 + 标记索引) 需要动态分配,通常为 malloc/free 或内部池化
访问速度 直接访问(std::get 需要类型检查 + 可能的动态分配
复制/移动 复制代价与类型大小相关 复制时动态分配,代价更大

在高性能场景,std::variant 通常更优,尤其当类型集合固定且访问频繁时。std::any 由于每次访问都涉及类型检查,适合类型不确定、少量访问的情况。

3. 类型安全

  • std::variant 在编译期即确定了可接受的类型集合,任何错误的访问都会在编译阶段被捕获。
  • std::any 的安全性完全依赖运行时检查,一旦类型不匹配会抛异常,导致潜在的性能损失和异常处理成本。

4. 典型使用场景

4.1 std::variant

  1. 状态机
    enum class State { Idle, Running, Paused, Stopped };
    using StateInfo = std::variant<std::monostate, int, std::string>;
    StateInfo current;
    // 根据状态存放不同数据
    current = 42; // Idle 时存 int
  2. 解析不同数据类型
    在 JSON 解析器中,可以用 std::variant 表示 nullboolintdoublestd::string 等。
  3. 回调参数
    需要在回调中传递多种可能的参数类型时,使用 std::variant 可以避免裸 void*

4.2 std::any

  1. 插件系统
    当插件提供的接口类型未知时,可以使用 std::any 作为通用参数容器。
  2. 跨平台消息总线
    消息可以携带任意类型的数据,使用 std::any 可以在不破坏类型安全的前提下存储。
  3. 动态配置
    配置文件解析后,每个键对应的值类型未知,可用 std::any 存储。

5. 实战示例

下面给出一个使用 std::variant 解析简单表达式的示例。

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

using Token = std::variant<std::string, double>;

std::vector <Token> tokenize(const std::string& expr) {
    std::vector <Token> tokens;
    size_t i = 0;
    while (i < expr.size()) {
        if (std::isspace(expr[i])) { ++i; continue; }
        if (std::isdigit(expr[i]) || expr[i] == '.') {
            size_t j = i;
            while (j < expr.size() && (std::isdigit(expr[j]) || expr[j] == '.')) ++j;
            tokens.emplace_back(std::stod(expr.substr(i, j - i)));
            i = j;
        } else {
            tokens.emplace_back(std::string(1, expr[i]));
            ++i;
        }
    }
    return tokens;
}

double evaluate(const std::vector <Token>& tokens) {
    std::stack <double> values;
    std::stack<std::string> ops;

    auto apply = [](double a, double b, const std::string& op) -> double {
        if (op == "+") return a + b;
        if (op == "-") return a - b;
        if (op == "*") return a * b;
        if (op == "/") return a / b;
        throw std::runtime_error("unknown operator");
    };

    for (const auto& tk : tokens) {
        if (std::holds_alternative <double>(tk)) {
            values.push(std::get <double>(tk));
        } else {
            std::string op = std::get<std::string>(tk);
            if (op == "(") {
                ops.push(op);
            } else if (op == ")") {
                while (!ops.empty() && ops.top() != "(") {
                    double b = values.top(); values.pop();
                    double a = values.top(); values.pop();
                    values.push(apply(a, b, ops.top()));
                    ops.pop();
                }
                if (!ops.empty() && ops.top() == "(") ops.pop();
            } else { // operator
                while (!ops.empty() &&
                       ops.top() != "(" &&
                       ((op == "+" || op == "-") || (op == "*" || op == "/"))) {
                    double b = values.top(); values.pop();
                    double a = values.top(); values.pop();
                    values.push(apply(a, b, ops.top()));
                    ops.pop();
                }
                ops.push(op);
            }
        }
    }

    while (!ops.empty()) {
        double b = values.top(); values.pop();
        double a = values.top(); values.pop();
        values.push(apply(a, b, ops.top()));
        ops.pop();
    }
    return values.top();
}

int main() {
    std::string expr = "3 + 4 * (2 - 1) / 5";
    auto tokens = tokenize(expr);
    std::cout << "Result: " << evaluate(tokens) << '\n';
    return 0;
}

关键点

  • Tokenstd::variant<std::string, double>,既能保存操作符也能保存数字。
  • std::holds_alternativestd::get 用于安全地访问具体类型。
  • 由于 Token 的类型集合是固定的,编译器在 evaluate 函数中可以对每种类型的处理进行优化。

6. 何时选 std::variant,何时选 std::any

需求 推荐选择
需要 编译期类型安全固定类型集合 std::variant
需要存储 任意类型,且类型集合未知或会频繁变化 std::any
需要 高性能低延迟的数值运算 std::variant
需要 跨模块插件化的通用数据容器 std::any

7. 小结

  • std::variantstd::any 各有优势,关键在于 类型集合的可预知性运行时安全
  • 在大多数业务逻辑中,如果类型集合固定且访问频繁,优先考虑 std::variant
  • 对于需要高度通用、类型未知的场景,std::any 更加灵活,但要注意异常处理和性能开销。

通过合理选择这两种工具,你可以在 C++17 代码中既保持类型安全,又能充分发挥语言的优势。祝编码愉快!

发表评论