数据类型 Variant
[TOC]
1. union
1.1 union 的使用
union 是 C++ 中一个较老的特性,它允许在相同的内存位置存储不同的数据类型,但一次只能存储其中一种类型的值。union 不需要额外的赋值和强制类型转换,同一个数据可解释为两个不一样的东西。
union 的定义格式:1
1
2
3
4
union/*共用体名*/
{
/*成员列表*/
}/*共用体变量名*/;
-
简单示例:2
1 2 3 4 5 6 7 8 9 10 11 12 13 14
union MyUnion { int intValue; float floatValue; }; void main() { MyUnion u; u.intValue = 5; std::cout << "Integer value: " << u.intValue << std::endl; // 现在存储了一个float值,上面赋的intValue的值将变得无效 u.floatValue = 3.14f; std::cout << "Float value: " << u.floatValue << std::endl; }
-
使用
union对数据进行打包:11 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
void main(void) { // 定义一个联合体 union ByteSplit { unsigned int word; unsigned char byte[4]; } data; // 给联合体的字节成员赋值 data.byte[0] = 0x11; data.byte[1] = 0x22; data.byte[2] = 0x33; data.byte[3] = 0x44; // 输出整数和字节 printf("整数值为:%u\n", data.word); }
-
使用
union优化空间利用:1在大多数编程环境中,
union通常用于空间优化。由于union的所有成员共享同一块内存空间,因此可以通过使用不同的数据类型来优化内存使用。以下是一个CANopen的例子:在Canopen 中,有一种PDO的通信形式,他就相当于预定义好了一条CAN 帧中 8 个自己的数据意义。我们可以简单的理解为,不同的
id对应了不同类型数据的具体意义,比如id ==1时代表4个速度变量,id ==1代表4个温度变量。也就是说,发送的消息并不会同时要包含速度和温度,而是每个帧分开来的发送的,这个时候,我们就可以采用
union的特性来构造一个数据结构,这样做的好处是能够缩减变量占用的内存。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
struct buffer { short id; /*CAN——ID*/ union { struct { short speed1; short speed2; short speed3; short speed4; } speed; struct { short temp1; short temp2; short temp3; short temp4; } temp; }Info; }my_buff;
采用上述的结构的话,我们可以计算一下,结构体占用的存储空间是 10个字节,如果我们分别为温度和速度的解析来定义缓存变量,那么占用的内存空间将几乎增大一倍。
这里有了 id的加入,我们可以在接收端对数据进行解析了。
通过上述的这个例子可以了解,如果不使用 union 的话,在进行数据传输的时候,直接将由 struct 构造的数据形成数据帧发送过去,发送的数据包要比使用 union 构造的数据大不少,使用 union 构造数据,既能够帮助我们节省了存储空间,还节省了通信时的带宽。
1.2 union 的问题
- 使用
union时,程序员需要确保正确地使用了当前存储的类型。不正确的使用可能导致数据损坏或未定义行为。2 - 无法知道当前使用的类型是什么,因此通常需要额外的变量来追踪。23
union无法自动调用底层数据成员的析构函数3。所以union不能存储有复杂构造函数或析构函数的类型(例如,标准库中的容器类型)。2
2. std::variant
std::variant 是 C++17 引入的一个类型安全的联合体(union)。它可以存储并操作几种不同类型中的一个,类似于传统的联合体(union)。
但比传统联合体提供了更高的类型安全性和更方便的接口,并且知道它自己当前存储的是哪种类型。
当存储的类型有非平凡的构造函数或析构函数时,std::variant 可以处理异常,而 union 不能。
但std::variant 存在一定的限制:虽然它可以存储多种类型,但在任何给定时间点,它只能存储其中一种。这就像是一个变色的蜥蜴,虽然它可以变成多种颜色,但一次只能是一种。4
简单来说,std::variant 提供了比传统 union 更安全、更灵活的方式来处理存储多种类型的需求。尽管 std::variant 可能在性能和内存占用上略微不如 union,但其带来的类型安全和易用性在许多情况下是值得的。2
下面是一个基本用法的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
#include <variant>
using MyVariant = std::variant<int, double, std::string>;
int main()
{
MyVariant v1 = 42;
MyVariant v2 = 3.14;
MyVariant v3 = "hello";
// 访问存储的值(不安全,需确保类型正确)
std::cout << std::get<int>(v1) << std::endl;
// 安全地访问存储的值
if (auto pval = std::get_if<int>(&v1))
{
std::cout << *pval << std::endl;
}
return 0;
}
在上面的例子中,MyVariant 可以存储 int 、 float 或 std::string 类型的数据。
不同于 union,std::variant 保留了存储的数据类型的信息,并提供了类型安全的访问方法 get_if()。
3. std::visit
std::visit 是 C++17 中引入的一个工具,用于访问和操作存储在 std::variant 类型中的数据。
std::visit 的工作原理见参考文章4。
3.1 基本接口与使用原理
std::visit 的基本接口如下:
1
2
template<class Visitor, class... Variants>
constexpr visit(Visitor&& vis, Variants&&... vars);
- Visitor:一个可调用对象,它应该能够接受
Variants中每种类型的值。它通常是一个重载了operator()的结构或类。 - Variants:一个或多个
std::variant类型的对象。
使用 std::visit 的典型方式是:4
-
定义一个结构体、类或lamda函数(也就是Visitor),Visitor中重载了针对
std::variant可能持有的每种类型的operator()方法。 -
将这个Visitor的实例以及
std::variant对象传递给std::visit。 -
std::visit将自动调用与std::variant当前存储的值类型相匹配的重载方法。
3.2 使用方法:泛型 lambda 表达式
std::visit 允许传入一个可调用对象(callable object),通常是一个 lambda 表达式。
现代 C++ 提供了一种特殊的 lambda 表达式,称为泛型 lambda 表达式(generic lambda)。
泛型 lambda:
泛型 lambda 是一个使用
auto关键字作为参数类型的 lambda 表达式。这意味着 lambda 可以接受任何类型的参数,并在函数体内进行处理。
1 2 3 auto generic_lambda = [](auto x) { // do something with x };
这种灵活性在处理 std::variant 时尤为有用,因为你可能需要根据多种可能的类型来编写逻辑。
3.3 使用方法:泛型 lambda 与类型判断4
编程就像是一场高级的拼图游戏。你需要一种机制来判断哪块拼图适用于当前的情况。4
在 std::visit 的上下文中,这通常是通过 if constexpr5 和 类型萃取(type traits)6 来完成的。
通过结合 if constexpr 和类型萃取,可以写出高度灵活且类型安全的代码。
我们可以把 泛型lambda 与 类型判断 融合到一起,优雅地使用 std::visit。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
std::variant<int, double, std::string> v = "hello";
std::visit([](auto&& arg)
{
// 类型萃取:
// 定义一个新类型T,它是传递给它的arg参数的去引用、去cv限定符版本的类型。
// 例如,如果arg是一个const int&类型的参数,T 就会是int
using T = std::decay_t<decltype(arg)>;
// 条件编译:
if constexpr (std::is_same_v<T, int>) {
std::cout << "int: " << arg << std::endl;
}
else if constexpr (std::is_same_v<T, double>) {
std::cout << "double: " << arg << std::endl;
}
else {
// 编译代码时检查T是不是std::string
static_assert(std::is_same_v<T, std::string>);
std::cout << "string: " << arg << std::endl;
}
}, v);
这里,我们使用了泛型 lambda 来接受任何类型的 arg,然后用 if constexpr 和类型萃取来确定 arg 的实际类型,并据此执行相应的操作。
3.4 使用方法:访问者模式
下面是一个简单的 std::visit 使用示例。
在这个例子中使用 std::variant 来存储不同类型的数据,并展示如何使用 std::visit 以类型安全的方式访问和处理这些数据。
假设我们有一个 std::variant,它可以存储一个 int、一个 double 或一个 std::string 类型的值。我们将编写一个访问者函数对象,这个对象会根据 std::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
#include <iostream>
#include <variant>
#include <string>
#include <functional>
// 定义 variant 类型
using MyVariant = std::variant<int, double, std::string>;
// 访问者函数对象
struct VariantVisitor {
void operator()(int i) const {
std::cout << "处理 int: " << i << std::endl;
}
void operator()(double d) const {
std::cout << "处理 double: " << d << std::endl;
}
void operator()(const std::string& s) const {
std::cout << "处理 string: " << s << std::endl;
}
};
int main() {
MyVariant v1 = 10; // v1 存储 int
MyVariant v2 = 3.14; // v2 存储 double
MyVariant v3 = "hello"; // v3 存储 string
std::visit(VariantVisitor(), v1); // 输出: 处理 int: 10
std::visit(VariantVisitor(), v2); // 输出: 处理 double: 3.14
std::visit(VariantVisitor(), v3); // 输出: 处理 string: hello
return 0;
}
在这个例子中:
- 我们定义了一个
std::variant类型MyVariant,它可以存储int、double或std::string。 VariantVisitor是一个重载了operator()的结构体,对每种可能的类型提供了一个处理方法。- 在
main函数中,我们创建了三个MyVariant实例,分别存储不同的类型。 - 使用
std::visit调用VariantVisitor实例,它会自动选择并调用与variant当前存储的类型相匹配的重载函数。
这个例子展示了 std::visit 如何提供一种类型安全、灵活的方式来处理存储在 std::variant 中的不同类型的数据。
参考文章7891011
-
【C++ 17 新功能 std::visit 】深入解析 C++17 中的 std::visit:从原理到实践 ↩︎ ↩︎2 ↩︎3 ↩︎4 ↩︎5
-
在C++17中引入的
if constexpr是一个条件编译语句,它允许编译器在编译时根据条件表达式决定是否编译某段代码。if constexpr是模板元编程中特别有用的特性,它可以基于模板参数进行条件编译,从而在编译期间就消除不必要的分支和相关代码。 constexpr.md ↩︎ -
类型萃取(Type Traits)是 C++11 引入的一组模板,用于在编译时获取类型的属性,就是把类型提取出来。类型萃取.md ↩︎