C++C++17类型安全的联合体 -- std::variant
苏丙榅1. std::variant 概述
想象你有一个魔法盒子,这个盒子同时只能装一种东西,但这个东西可以是不同类型的,比如:今天装一个整数,明天装一个字符串,后天装一个浮点数。这个魔法盒子就是 std::variant。std::variant 是 C++17 引入的类型安全的联合体。与传统的 union 不同,它有以下特点:
- 多选一:在定义时必须告诉它,它只能存哪些类型(比如
int, double, string)。不能存这三种类型以外的东西。
- 类型安全:它永远知道自己当前存的是哪个类型。如果试图用错误的类型去取数据,编译器会在编译期帮我们检查,或者直接抛出异常,绝不会产生未定义行为。
- 高效:大多数情况下,
std::variant 的大小就是其所有成员中最大的那个大小(加上一点点开销),它通常存储在栈上,不需要像 std::any 那样动态分配内存。
std::variant类的头文件和基础声明如下:
1 2 3
| #include <variant>
std::variant<int, double, std::string> v;
|
注意:std::variant 不允许是空的(默认情况下),它至少必须列出一个类型。如果你声明了它但没有赋值,默认会调用第一个类型的默认构造函数。所以上面 v 默认存的是一个 int 值 0。
std::variant 类 API 在线查询
2. std::variant 操作
2.1 赋值与访问
我们可以像使用普通变量一样赋值。访问时,主要使用 index()(索引)和 get()(获取)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| #include <iostream> #include <variant> #include <string>
int main() { std::variant<int, double, std::string> v;
std::cout << "Index: " << v.index() << ", Value: " << std::get<int>(v) << std::endl;
v = 3.14; std::cout << "Index: " << v.index() << ", Value: " << std::get<double>(v) << std::endl;
v = "Hello C++"; std::cout << "Index: " << v.index() << ", Value: " << std::get<std::string>(v) << std::endl;
return 0; }
|
注意:index() 返回当前激活类型在模板参数列表中的位置(从 0 开始)。
2.1.1 std::get 的危险与 std::get_if 的温柔
如果我们使用 std::get 去取一个当前不存在的类型,程序会直接抛出 std::bad_variant_access 异常。这对调试很有用,但在生产环境可能导致崩溃。更安全的做法是使用 std::get_if,它返回一个指针。如果类型不对,它返回 nullptr,而不会抛出异常。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| #include <iostream> #include <variant> #include <string>
int main() { std::variant<int, double> v = 42;
if (auto ptr = std::get_if<double>(&v)) { std::cout << "获取到 double: " << *ptr << std::endl; } else { std::cout << "当前不是 double,指针为空" << std::endl; }
if (auto ptr = std::get_if<int>(&v)) { std::cout << "获取到 int: " << *ptr << std::endl; }
return 0; }
|
2.1.2 进阶访问 - std::visit
想象一下,如果我们想打印 variant 里的值,必须得写一堆 if-else 或者 switch这太麻烦了!如果 variant 定义了 10 种类型,你得写 10 个分支。
C++17 提供的std::visit 允许我们传入一个可调用对象(函数、Lambda),它会自动识别 variant 当前存的是什么类型,然后调用对应的函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| #include <iostream> #include <variant> #include <string> #include <vector>
struct Printer { void operator()(int i) { std::cout << "Int: " << i << std::endl; } void operator()(double d) { std::cout << "Double: " << d << std::endl; } void operator()(const std::string& s) { std::cout << "String: " << s << std::endl; } };
int main() { std::variant<int, double, std::string> v; v = 100; std::visit(Printer{}, v);
v = 3.14; std::visit(Printer{}, v);
v = "Test"; std::visit(Printer{}, v);
auto generic_printer = [](auto&& arg) { std::cout << "Value: " << arg << ", Type size: " << sizeof(arg) << std::endl; }; v = 42; std::visit(generic_printer, v);
return 0; }
|
使用 std::visit 甚至可以同时传入多个 variant进行处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| #include <iostream> #include <variant> #include <typeinfo>
int main() { std::variant<int, double> v1 = 10; std::variant<int, double> v2 = 2.0;
std::visit([](auto&& a, auto&& b) { auto result = a + b; std::cout << a << " + " << b << " = " << result << std::endl; std::cout << "结果类型是: " << typeid(result).name() << std::endl; }, v1, v2);
return 0; }
|
虽然 v1 是 int,但因为它加上了 double 类型的 v2,最终结果被提升为了 double。
2.2 特殊情况与异常处理
前面说过 variant 不能为空。但如果你确实需要表示没有值的状态怎么办?(比如数据库查询结果为空)。C++17 提供了一个特殊的空类型 std::monostate。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| #include <variant> #include <iostream>
int main() { std::variant<std::monostate, int, std::string> v; if (std::holds_alternative<std::monostate>(v)) { std::cout << "Variant 目前是空的" << std::endl; } v = 10; if (std::holds_alternative<int>(v)) { std::cout << "Variant 现在有一个整数" << std::endl; } return 0; }
|
std::holds_alternative 是 <variant> 头文件中的一个非常实用的辅助函数。它的作用是检查 std::variant 对象当前是否持有特定类型的值。函数原型为:
1 2
| template <class T, class... Types> constexpr bool holds_alternative(const std::variant<Types...>& v) noexcept;
|
- 参数:
- 第一个模板参数
T:想要检查的类型(比如 int、string,甚至是 std::monostate)。
- 参数
v:要检查的那个 variant 对象。
- 返回值:
true:如果 v 存储的值为 T 类型。
false:如果 v 存储的是其他类型。
std::holds_alternative 实际上是 index() 的一个更方便、更易读的封装版本。
| 方法 |
写法 |
优点 |
缺点 |
index() |
v.index() == 1 |
性能极高(直接查索引) |
可读性差(需要记住 1 代表 int),如果类型顺序变了,代码容易挂。 |
holds_alternative |
holds_alternative<int>(v) |
可读性极好(直接看代码就知道在查 int 类型),维护方便 |
编译器需要知道类型索引,可能会增加一点点编译时间(运行时性能通常没区别)。 |
- 如果需要遍历所有可能的情况(比如
switch 语句),或者追求极致的编译期已知逻辑,用 index()。
- 如果只是想在运行前做判断(比如:如果存的是
string 我就去解析,如果是 int 我就去计算),强烈推荐使用 std::holds_alternative,代码更安全、更清晰。
说完了std::variant中的取值和类型判断,我们再来思考另外一个问题:如果你给 variant 赋值,但在构造新值的过程中抛出了异常(比如内存不足),variant 会发生什么呢?
C++17 保证如果赋值失败,旧值不会被破坏,variant 依然保持旧值有效,或者变成空(异常安全保证)。
3. std::variant 实践
下面是一个模拟简单计算器的综合示例,展示了 variant 的实际威力。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89
| #include <iostream> #include <variant> #include <string> #include <stdexcept>
struct DivideByZeroError {};
using CalcResult = std::variant<double, DivideByZeroError>;
struct OpAdd {}; struct OpSub {}; struct OpMul {}; struct OpDiv {};
using Operator = std::variant<OpAdd, OpSub, OpMul, OpDiv>;
CalcResult calculate(double a, Operator op, double b) { return std::visit([a, b](auto&& op_tag) -> CalcResult { using T = std::decay_t<decltype(op_tag)>; if constexpr (std::is_same_v<T, OpAdd>) { return a + b; } else if constexpr (std::is_same_v<T, OpSub>) { return a - b; } else if constexpr (std::is_same_v<T, OpMul>) { return a * b; } else if constexpr (std::is_same_v<T, OpDiv>) { if (b == 0.0) { return DivideByZeroError{}; } return a / b; } }, op); }
int main() { double x = 10.0; double y = 2.0; Operator op = OpDiv{}; auto result = calculate(x, op, y);
std::visit([](auto&& arg) { using T = std::decay_t<decltype(arg)>; if constexpr (std::is_same_v<T, double>) { std::cout << "计算结果是: " << arg << std::endl; } else if constexpr (std::is_same_v<T, DivideByZeroError>) { std::cout << "错误:除数不能为零!" << std::endl; } }, result);
y = 0.0; result = calculate(x, op, y); std::visit([](auto&& arg) { using T = std::decay_t<decltype(arg)>; if constexpr (std::is_same_v<T, double>) { std::cout << "计算结果是: " << arg << std::endl; } else if constexpr (std::is_same_v<T, DivideByZeroError>) { std::cout << "错误:除数不能为零!" << std::endl; } }, result);
return 0; }
|
std::variant 是 C++17 引入的最重要的数据结构之一,它让类型安全的联合体成为现实。通过配合 std::visit 和 if constexpr,我们可以写出既高效又优雅的代码,完全摆脱老旧的 switch-case 和不安全的 void*。