在 C++17 标准中,为了解决类型安全多态值存储的问题,标准库提供了两种主要的工具:std::variant 和 std::any。它们看似相似,但在设计理念、使用场景、类型安全以及性能上有显著差异。本文将通过对比两者的特点、典型应用以及实际代码示例,帮助你在项目中做出更合适的选择。
1. 基本概念
-
std::variant<T...>- 采用类型擦除与变体实现,内部维护一个固定的类型集合。
- 只能存放预先声明的类型之一,访问时必须使用
std::get、std::visit或std::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
- 状态机:
enum class State { Idle, Running, Paused, Stopped }; using StateInfo = std::variant<std::monostate, int, std::string>; StateInfo current; // 根据状态存放不同数据 current = 42; // Idle 时存 int - 解析不同数据类型:
在 JSON 解析器中,可以用std::variant表示null、bool、int、double、std::string等。 - 回调参数:
需要在回调中传递多种可能的参数类型时,使用std::variant可以避免裸void*。
4.2 std::any
- 插件系统:
当插件提供的接口类型未知时,可以使用std::any作为通用参数容器。 - 跨平台消息总线:
消息可以携带任意类型的数据,使用std::any可以在不破坏类型安全的前提下存储。 - 动态配置:
配置文件解析后,每个键对应的值类型未知,可用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;
}
关键点
Token是std::variant<std::string, double>,既能保存操作符也能保存数字。std::holds_alternative与std::get用于安全地访问具体类型。- 由于
Token的类型集合是固定的,编译器在evaluate函数中可以对每种类型的处理进行优化。
6. 何时选 std::variant,何时选 std::any
| 需求 | 推荐选择 |
|---|---|
| 需要 编译期类型安全、固定类型集合 | std::variant |
| 需要存储 任意类型,且类型集合未知或会频繁变化 | std::any |
| 需要 高性能、低延迟的数值运算 | std::variant |
| 需要 跨模块、插件化的通用数据容器 | std::any |
7. 小结
std::variant和std::any各有优势,关键在于 类型集合的可预知性 与 运行时安全。- 在大多数业务逻辑中,如果类型集合固定且访问频繁,优先考虑
std::variant。 - 对于需要高度通用、类型未知的场景,
std::any更加灵活,但要注意异常处理和性能开销。
通过合理选择这两种工具,你可以在 C++17 代码中既保持类型安全,又能充分发挥语言的优势。祝编码愉快!