【Example】C++ 标准库 std::variant (类型安全的联合体)
Warning:
这是一篇试验性质文章,基于我以前的文章风格与相关资料由 LLM 生成,旨意是用作【快速入门】及 Copilot 等工具【辅助开发的 RAG 资料】。
在 C++ 的编程实践中,我们有时会遇到这样一种需求:一个变量在不同时刻可能需要存储不同类型的值。传统的 C 语言 union
可以实现这一点,但它缺乏类型安全,开发者需要自行追踪当前存储的究竟是哪种类型,稍有不慎就可能导致未定义行为。为了解决这个问题,C++17 标准库引入了 std::variant
。你可以把它想象成一个“魔法盒子”,这个盒子在任何时刻只能精确地装着你预先定义好的一系列类型中的某一种,并且它自身清楚地知道里面装的是什么。
类模板
std::variant
表示一个类型安全的联合体。std::variant
的一个实例在任意时刻要么保有其一个可选类型之一的值,要么在错误情况下无值(此状态难以达成,见valueless_by_exception
)。----- C++ Reference
而这个盒子的大小由它可能要装的最大的那个东西决定。当你往里面放东西时,东西就直接放在盒子里,盒子本身不会为了这个东西再去动态申请额外的空间。而且,这个盒子对能放什么东西有一些规定(不能是引用、C 数组、void),也不能是“空盒子”(除非你用 std::monostate
特指一个空状态)。
头文件:
#include <variant>
构造 std::variant
std::variant
提供了多种构造方式,让初始化过程既灵活又安全。
1. 默认构造函数
默认构造的 variant
会保有其第一个模板参数类型的值,并进行值初始化。前提是第一个类型必须是可默认构造的。
// 默认构造,v1 将包含一个默认构造的 int (值为 0)
std::variant<int, float, std::string> v1;
// 如果第一个类型不能默认构造,编译会失败
// std::variant<MyNonDefaultConstructible, int> v_fail;
如果希望一个 variant
的第一个类型不可默认构造,但仍希望 variant
本身可以默认构造,可以使用 std::monostate
作为第一个类型:
#include <variant>
#include <string>
#include <iostream>
struct MyNonDefaultConstructible {
MyNonDefaultConstructible(int) {}
};
int main() {
// 使用 std::monostate 使其可默认构造
// v_mono 将处于持有 std::monostate 的状态
std::variant<std::monostate, MyNonDefaultConstructible, std::string> v_mono;
if (std::holds_alternative<std::monostate>(v_mono)) {
std::cout << "v_mono holds monostate" << std::endl;
}
return 0;
}
2. 拷贝与移动构造函数
std::variant
支持标准的拷贝和移动构造。
std::variant<int, std::string> v_orig = "Hello";
std::variant<int, std::string> v_copy = v_orig; // 拷贝构造
std::variant<int, std::string> v_move = std::move(v_orig); // 移动构造
这要求所有备选类型都是可拷贝/移动构造的。
3. 转换构造函数 (直接用值初始化)
这是最常见的初始化方式之一。你可以直接用备选类型之一的值来初始化 variant
。编译器会根据传入值的类型选择合适的备选类型。
std::variant<int, float, std::string> v_int = 10; // v_int 持有 int
std::variant<int, float, std::string> v_float = 3.14f; // v_float 持有 float
std::variant<int, float, std::string> v_str = "World"; // v_str 持有 std::string
// 注意歧义:
// std::variant<long, int> v_amb = 0; // 编译错误,0 可以转换为 long 或 int
std::variant<long, int> v_long = 0L; // 明确指定为 long
std::variant<const char*, std::string> v_char_ptr = "Raw C-string"; // 选择 const char*
转换构造函数在选择备选类型时有一套明确的规则,通常会选择“最佳匹配”。如果存在歧义,编译会失败。
4. std::in_place_type_t<T>
和 std::in_place_index_t<I>
精确构造
当你需要精确指定 variant
初始化为哪个类型,或者需要传递参数给备选类型的构造函数时,in_place_type
和 in_place_index
就派上用场了。
std::in_place_type_t<T>
: 指定要构造的类型T
。std::in_place_index_t<I>
: 指定要构造的类型的索引I
(从0开始)。
#include <variant>
#include <string>
#include <iostream>
struct Point {
int x, y;
Point(int x_val, int y_val) : x(x_val), y(y_val) {
std::cout << "Point(" << x << ", " << y << ") constructed" << std::endl;
}
};
int main() {
// 使用 std::in_place_type_t
std::variant<int, std::string, Point> v_point(std::in_place_type_t<Point>{}, 5, 10);
// 或者简写 (C++17 class template argument deduction):
// std::variant v_point(std::in_place_type<Point>, 5, 10);
// 使用 std::in_place_index_t
std::variant<int, std::string, Point> v_str_idx(std::in_place_index_t<1>{}, "Hello from index");
// 或者简写:
// std::variant v_str_idx(std::in_place_index<1>, "Hello from index");
if (std::holds_alternative<Point>(v_point)) {
std::cout << "v_point holds Point" << std::endl;
}
if (std::holds_alternative<std::string>(v_str_idx)) {
std::cout << "v_str_idx holds: " << std::get<std::string>(v_str_idx) << std::endl;
}
return 0;
}
这些构造方式消除了转换构造可能带来的歧义,并允许直接构造非拷贝/移动的类型。
成员函数
名称 | 说明 |
---|---|
(constructor) |
构造 variant 对象 (如上所述) |
(destructor) |
销毁 variant 及其所含的值 |
operator= |
赋值给 variant |
index() |
返回 variant 所保有可选项的零基下标。若因异常无值,则返回 variant_npos 。 |
valueless_by_exception() |
检查 variant 是否因异常而处于无值状态。 |
emplace<T>(args...) |
原位构造类型为 T 的值。T 必须是备选类型之一且唯一。 |
emplace<I>(args...) |
原位构造索引为 I 的备选类型的值。 |
swap(other) |
与另一个 variant 交换内容。 |
非成员函数 (访问与操作 std::variant
)
这些函数是与 std::variant
交互的核心。
名称 | 说明 |
---|---|
std::holds_alternative<T>(v) |
检查 variant v 是否当前持有类型 T 。 |
std::get<I>(v) |
返回 variant v 中索引为 I 的值的引用。如果 v.index() != I ,抛出 std::bad_variant_access 。 |
std::get<T>(v) |
返回 variant v 中类型为 T 的值的引用。如果 v 不持有 T ,抛出 std::bad_variant_access 。T 必须唯一。 |
std::get_if<I>(&v) |
如果 v->index() == I ,返回指向 v 中索引为 I 的值的指针;否则返回 nullptr 。不抛异常。 |
std::get_if<T>(&v) |
如果 v 持有类型 T ,返回指向 v 中类型为 T 的值的指针;否则返回 nullptr 。T 必须唯一。不抛异常。 |
std::visit(visitor, variants...) |
对一个或多个 variant 中当前持有的值调用 visitor 。 |
std::swap(v1, v2) |
特化 std::swap 算法,交换两个 variant 的内容。 |
辅助类与常量
名称 | 说明 |
---|---|
std::monostate |
用作非可默认构造类型的 variant 的首个可选项的占位符类型,或表示空状态。 |
std::bad_variant_access |
非法地访问 variant 的值时(如 std::get 类型不匹配)抛出的异常。 |
std::variant_npos |
variant 处于 valueless_by_exception 状态时的索引值。 |
std::variant_size<VariantType> |
编译时获取 variant 可选项的数量。 |
std::variant_size_v<VariantType> |
variant_size 的变量模板版本 (C++17)。 |
std::variant_alternative<I, VariantType> |
编译时获取 variant 中索引为 I 的可选项的类型。 |
std::variant_alternative_t<I, VariantType> |
variant_alternative 的别名模板版本 (C++17)。 |
std::hash<std::variant<Types...>> |
std::hash 的特化,允许 std::variant 作为无序关联容器的键。 |
安全获取 std::variant
中的值
获取 std::variant
中的值时,类型安全是首要考虑的。
1. 使用 std::holds_alternative
和 std::get
(需要注意异常)
#include <variant>
#include <string>
#include <iostream>
int main() {
std::variant<int, std::string> var = "Hello";
if (std::holds_alternative<std::string>(var)) {
std::cout << "Holds string: " << std::get<std::string>(var) << std::endl;
} else if (std::holds_alternative<int>(var)) {
std::cout << "Holds int: " << std::get<int>(var) << std::endl;
}
var = 123;
try {
// 尝试获取错误的类型
std::cout << "Trying to get string: " << std::get<std::string>(var) << std::endl;
} catch (const std::bad_variant_access& e) {
std::cerr << "Exception: " << e.what() << std::endl;
if (std::holds_alternative<int>(var)) {
std::cout << "Actually holds int: " << std::get<int>(var) << std::endl;
}
}
return 0;
}
虽然可行,但 std::get
在类型不匹配时会抛出异常,这可能不是最高效或最优雅的方式。
2. 使用 std::get_if
(推荐的安全方式)
std::get_if
不会抛出异常,而是返回一个指针。如果类型匹配,返回指向值的指针;否则返回 nullptr
。
#include <variant>
#include <string>
#include <iostream>
int main() {
std::variant<int, std::string> var;
var = "World";
// 尝试获取 string
if (auto* str_ptr = std::get_if<std::string>(&var)) {
std::cout << "Holds string: " << *str_ptr << std::endl;
*str_ptr += "!"; // 可以修改值
} else {
std::cout << "Does not hold string." << std::endl;
}
std::cout << "Current string value: " << std::get<std::string>(var) << std::endl;
var = 42;
// 尝试获取 int (使用索引)
if (auto* int_ptr = std::get_if<0>(&var)) { // 0 是 int 的索引
std::cout << "Holds int: " << *int_ptr << std::endl;
} else {
std::cout << "Does not hold int." << std::endl;
}
// 尝试获取 string (此时var持有int)
if (auto* str_ptr_again = std::get_if<std::string>(&var)) {
std::cout << "This won't be printed: " << *str_ptr_again << std::endl;
} else {
std::cout << "Still does not hold string, as expected." << std::endl;
}
return 0;
}
std::get_if
是进行条件访问的首选方法。
3. 使用 std::visit
(最通用和强大的方式)
std::visit
接受一个可调用对象 (Visitor) 和一个或多个 variant
对象。它会根据 variant
(或多个 variant
组合) 当前持有的类型,以类型安全的方式调用 Visitor。
#include <variant>
#include <string>
#include <vector>
#include <iostream>
// 辅助结构体,用于创建重载的 lambda 集合 (C++17 Overload Pattern)
template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>; // 推导指引 (C++20 起可选)
int main() {
std::variant<int, double, std::string> my_variant;
my_variant = "Hello Variant!";
// 使用 lambda 作为 visitor
std::visit([](const auto& value) {
// auto& 推断出 variant 中实际存储的类型
using T = std::decay_t<decltype(value)>;
if constexpr (std::is_same_v<T, int>) {
std::cout << "It's an int: " << value << std::endl;
} else if constexpr (std::is_same_v<T, double>) {
std::cout << "It's a double: " << value << std::endl;
} else if constexpr (std::is_same_v<T, std::string>) {
std::cout << "It's a string: " << value << std::endl;
}
}, my_variant);
my_variant = 100;
// 使用重载的 lambda 集合
std::visit(overloaded{
[](int i) { std::cout << "Overloaded: int " << i << std::endl; },
[](double d) { std::cout << "Overloaded: double " << d << std::endl; },
[](const std::string& s) { std::cout << "Overloaded: string " << s << std::endl; }
}, my_variant);
// visit 也可以返回值
std::string type_name = std::visit(overloaded{
[](int) -> std::string { return "int"; },
[](double) -> std::string { return "double"; },
[](const std::string&) -> std::string { return "std::string"; }
}, my_variant);
std::cout << "Variant currently holds type: " << type_name << std::endl;
// 处理 valueless_by_exception (虽然很难主动触发,但可以了解)
// 假设 var_valueless 处于此状态
// std::visit(visitor, var_valueless); // 会抛出 std::bad_variant_access
return 0;
}
std::visit
非常强大,尤其是在需要对 variant
中所有可能的类型执行不同操作时。配合 if constexpr
或重载的函数对象/lambda,可以写出非常清晰和类型安全的代码。
valueless_by_exception
状态
在极少数情况下(通常是赋值或 emplace
过程中,新值的构造函数抛出异常,且旧值的析构和新值的构造不是原子操作时),std::variant
可能会进入一个“因异常而无值”的状态。此时,valueless_by_exception()
返回 true
,index()
返回 std::variant_npos
。任何尝试通过 std::get
或 std::visit
访问其值的行为都会抛出 std::bad_variant_access
。
这种状态比较罕见,因为 std::variant
的操作通常被设计为具有强异常安全保证。
最简单示例
#include <iostream>
#include <variant>
#include <string>
#include <vector> // For a more complex visitor if needed
// Helper for visit (overload pattern)
template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;
struct Circle { double radius; };
struct Square { double side; };
struct Triangle { double base, height; };
using Shape = std::variant<Circle, Square, Triangle, std::monostate>;
void print_shape_info(const Shape& shape) {
std::visit(overloaded{
[](const Circle& c) { std::cout << "Circle with radius: " << c.radius << std::endl; },
[](const Square& s) { std::cout << "Square with side: " << s.side << std::endl; },
[](const Triangle& t) { std::cout << "Triangle with base " << t.base << " and height " << t.height << std::endl; },
[](std::monostate) { std::cout << "Shape is empty (monostate)." << std::endl; }
}, shape);
}
int main() {
Shape shape1 = Circle{5.0};
Shape shape2 = Square{3.0};
Shape shape3; // Defaults to std::monostate if it's the first type, or if Circle was not default constructible
// Here, if Circle was first and not default constructible, this would be an error
// unless monostate was first. Let's assume monostate is first for default construction.
// To be explicit for default construction with monostate as an option:
Shape shape_empty(std::in_place_type_t<std::monostate>{});
print_shape_info(shape1);
print_shape_info(shape2);
print_shape_info(shape_empty);
// Modifying a shape
shape1 = Triangle{4.0, 6.0};
print_shape_info(shape1);
// Safe access using get_if
if (auto* p_circle = std::get_if<Circle>(&shape2)) { // shape2 is Square
std::cout << "This will not print, shape2 is not a Circle." << std::endl;
} else if (auto* p_square = std::get_if<Square>(&shape2)) {
std::cout << "Safely got Square, side: " << p_square->side << std::endl;
p_square->side = 3.5; // Modify it
}
print_shape_info(shape2); // See modified value
// Using index
std::cout << "Index of current type in shape1: " << shape1.index(); // Triangle is likely index 2
if (shape1.index() == std::variant_npos) {
std::cout << " (valueless by exception!)" << std::endl;
} else {
std::cout << std::endl;
}
return EXIT_SUCCESS;
}
何时使用 std::variant
?
- 当你需要在编译时确定一个对象可以存储的有限几种类型之一时。
- 当你需要比
union
更类型安全的选择时。 - 与
std::any
不同,std::any
可以存储任何类型(在运行时确定),而std::variant
的类型集在编译时固定。这使得std::variant
通常更快,因为不需要类型擦除的开销。 - 与
std::optional
不同,std::optional<T>
表示一个T
类型的值可能存在也可能不存在,而std::variant
总是持有其备选类型之一的值(除非是valueless_by_exception
状态,或者你用std::monostate
来显式表示“空”状态)。
std::variant
是现代 C++ 中处理和类型值的一个强大工具,它通过编译时检查和丰富的 API 提供了优秀的类型安全性和表达能力。掌握它的使用,能让你的代码更加健壮和易于维护。
版权声明:
作者:芯片烤电池
链接:https://www.airchip.org.cn/index.php/2025/05/18/cpp-example-variant/
来源:芯片烤电池
文章版权归作者所有,未经允许请勿转载。
