C++ 的 pair 和 tuple

1 std::pair

1.1 C++ 98 的 std::pair

1.1.1 std::pair 的构造

​ C++ 的二元组 std::pair<> 在 C++ 98 标准中就存在了,其定义如下:

template<class T1, class T2> struct pair;

std::pair<> 是个类模板,它有两个成员:first 和 second,类型分别是模板参数指定的 T1 和 T2。可以用以下几种方法构造 std::pair<> 类型的变量(对象实例):

std::pair<int, double> ap1;  //默认的构造函数
std::pair<int, double> ap2(5,2.8); 
std::pair<int, double> ap3(ap2); //拷贝构造函数
ap1 = std::make_pair(6, 7.2);  //使用 make_pair函数
std::pair<int, std::string> p4{5, "ak47"};  //C++ 11 初始化列表
std::pair ap5(5, "ak47");  //C++ 17 推断指示语法,将在 1.3 节介绍

1.1.2 赋值与转换

对 std::pair<> 的访问也很简单,直接操作它的两个成员:

std::pair<int, double> ap(5,2.8); 
std::cout << ap.first << ", " << ap.second;
ap.first = 42;

如果你的数据中有两个数据耦合比较紧密,经常需要在一起成对出现,而你又不想额外定义一个 struct 的时候,可以考虑使用 std::pair<>。另外,std::pair<> 也可用于函数返回值的类型,这样就可以用一个 return 语句返回两个值。

​ 对于 C++ 来说,std::pair<char, int> 与 std::pair<int, char> 是两个完全不同的类型,它们之间的差别就像 std::string 和 std::vector 的差别一样大。一般来说,两个不同类型的 std::pair<> 变量是不能互相赋值的,但是如果两个 std::pair<> 变量对应的 first 和 second 属性能够对应进行隐式类型转换,则这样的赋值是允许的,比如:

std::pair<char, int> p1('A', 4);
std::pair<int, double> p2 = p1; //OK,隐式转换char -> int, int -> double

除了 C++ 内建的隐式转换,通过自定义构造函数进行的隐式转换也是可以的,比如:

struct FooTest {FooTest(int a){ value = std::format("{}", a);  }std::string value;
};std::pair<char, int> p1('A', 4);
std::pair<int, FooTest> p2 = p1; //OK, FooTest(int a) 构造函数完成隐式转换

1.1.3 比较

​ 两个 std::pair<> 变量可以互相比较大小,比较的原则就是先比较 first 属性,如果 first 属性的值相等(按照严格弱序比较)则继续比较 second 属性的值,来看个比较的例子:

std::pair<int, std::string> p1(5, "ak47");
std::pair<int, std::string> p2(5, "ak57");assert(p1 < p2); //5==5,但是 "ak47" < "ak57"

1.2 C++ 11 和 C++ 14 的改进

​ C++ 11 对 std::pair<> 进行了一些扩展,增加了一个成员函数 swap(),用于和另一个同类型的 std::pair<> 变量交换内容,比如:

std::pair<int, std::string> p1(42, "Hello");
std::pair<int, std::string> p2;
p2.swap(p1);
assert(p2.first == 42);
assert(p1.first == 0);

和其他类型一样,C++ 11 全局的 std::swap() 函数也支持 std::pair<> ,上面的交换代码也可以这样写:

std::swap(p1, p2);

​ C++ 11 提供的 std::get<> 函数也支持 std::pair<>,可以通过索引(0 或 1)获取一个 std::pair<> 变量的内容,C++ 14 又进行了补充,即可以根据类型匹配获取一个 std::pair<> 变量的内容,比如:

std::pair<int, std::string> p1(42, "Hello");assert(std::get<0>(p1) == std::get<int>(p1));
assert(std::get<1>(p1) == std::get<std::string>(p1));

需要注意,类型匹配的方式只适用于两个不同类型的数据组成的 pair。

​ 此外,一些用于 tuple 类型的操作也可以用于 std::pair<>,比如在编译期获取 std::tuple<> 类型中元素个数的 std::tuple_size,还有在编译期获取 std::tuple<> 类型中每个位置的元素类型的 std::tuple_element<N,T> 等等。对于 std::pair<> 来说,std::tuple_size 得到的值固定是 2,来看个例子:

std::cout << std::tuple_size<std::pair<int, std::string>>::value;  //输出 2std::tuple_element<0, std::pair<int, double>>::type a; //变量 a 的类型是 int

这两个方法配合,可以在编译期决断一些事情,比如这个例子:

template<class T>
void Test(const T& t) {int a[std::tuple_size<T>::value] = { 0 };  //定义数组typename std::tuple_element<0, T>::type myValue;myValue = t.first;
}std::pair<int, std::string> p1(5, "ak47");
Test(p1);  //此时 myValue 是 int 类型
Test(std::make_pair('A', 4));   //此时 myValue 是 char 类型

1.3 C++ 17 的推断指引

​ C++ 17 引入了推断指引(Deduction Guides)语法,当然,std::pair<> 也支持推断指引。没有推断指引的时候,构造一个 std::pair <>的对象实例需要指定具体的类型,也就是 std::pair<> 的两个模板参数,就是这样:

std::pair<int, std::string> p1(5, "ak47");

有了推断指引语法之后,代码就可以简化成这个样子:

std::pair p1(5, "ak47");

因为编译器能够从构造 p1 的两个参数中推断出它们的类型,所以就不需要显示指定具体的类型了。推断指引是个好东西,能少敲几次键盘,节省体力。

2 std::tuple

​ std::tuple 元组是 C++ 11 提供的标准库扩展,利用扩展的参数包语法,std::tuple 实现了对任意个数的非同质元素的聚合。元组是个好东西,有了它可以代替很多琐碎的、毫无价值的传统数据结构(struct)定义。同时,它还支持右值和移动语义,作为参数或返回值传递的时候,比某些构造不良的 struct 具有更好的效率。

2.1 std::tuple 的语法

2.1.1 std::tuple 的构造

​ std::tuple<> 是个模板类型,其定义如下:

template< class... Types >
class tuple;

class… Types 是参数包语法,Types 就是具体的类型列表。构造 std::tuple<> 对象实例可以借助于构造函数,也可以使用 std::make_tuple() 方法:

std::tuple<int, std::string, double> t1; //默认构造函数
std::tuple<int, std::string, double> t2 = {42, "hello", 2.7};
std::tuple<int, std::string, double> t3{ 42, "hello", 2.7 }; // C++ 11 初始化列表
std::tuple<int, std::string, double> t4(42, "hello", 2.7); //拷贝构造
t1 = t4; 
std::tuple<int, std::string, double> t5 = std::make_tuple(42, "hello", 2.7); //右值拷贝构造(move)
auto t5 = std::make_tuple(42, "hello", 2.7);  //等价于上一行
std::tuple t6(42, "hello", 2.7);  //C++ 17 的推断指示语法,将在 2.2 节介绍

​ 元组中可以使用引用类型,在构造元组的时候指定引用绑定的对象即可,绑定引用对象时可以使用 std::ref,也可以不使用:

int value = 3;
std::tuple<int&, std::string, double> t9(std::ref(value), "hello", 2.7);
//std::tuple<int&, std::string, double> t9(value, "hello", 2.7); 效果一样
std::get<0>(t9) = 4;
std::cout << "value=" << value << ", t9[0]=" << std::get<0>(t9) << std::endl;  //4,4

​ 需要注意的是,尽管一些过时的资料中提到 std::tuple<> 采用链式结构存放每个元素的值,但是实际情况并不是这样的。无论 GCC 还是 Visual C++,对元组的存储都是在内存中连续存放的,并且每个同类型的元组使用的内存大小是一样的。以 Visual C++ 为例,元组变量在内存中按照类型列表的倒序方式连续存储在一个内存块中,当然,如果一个对象中使用了指针属性,元组只存储这个对象的内容(包含指针),对象指针属性指向的内容则由对象自己负责存储和释放。

2.1.2 赋值和转换

​ std::tuple<> 的内部实现是借助于模板的递归推导机制做的,所以无法像 std::pair<> 那样提供成员属性用于访问元组内的各个元素,但是可以借助于同样模板化的 std::get() 方法访问和修改各个元素的值。来看下面的代码:

std::tuple<int, std::string, double> t1(42, "hello", 2.7); 
std::cout << std::get<0>(t1);  //输出 42
std::get<1>(t1) = "NiHao";
std::cout << std::get<1>(t1);  //输出 NiHao

注意,std::get() 中的模板参数 N 不支持动态绑定,即这样写代码是无法编译的:

std::tuple<int, std::string, double> t1(42, "hello", 2.7); for (int i = 0; i < 3; i++)std::cout << i + 1 << ": " << std::get<i>(t1) << std::endl;  // 编译错误

当然,可以使用 std::tuple_size 和 std::tuple_element<N,T> 在编译期获得元素的个数和元组各个元素的类型:

// 以下两行代码等价,都输出 3
std::cout << std::tuple_size<std::tuple<int, std::string, double>>::value << std::endl;
std::cout << std::tuple_size<std::tuple<int, std::string, double>>() << std::endl;std::tuple<int, std::string, double> t1(42, "hello", 2.7);
std::cout << std::tuple_size<decltype(t1)>::value << std::endl; //使用 decltypestd::tuple_element<2, std::tuple_size<std::tuple<int, std::string, double>>::type a; //double 类型
std::tuple_element<2, std::tuple_size<decltype(t1)>::type b; //double 类型

std::tuple<> 同样提供了 swap() 方法用于和另一个同类型(或可隐式转换)的 std::tuple<> 对象实例交换内容,当然全局的 std::swap() 方法也支持 std::tuple<>:

std::tuple<int, std::string, double> t1;
std::tuple<int, std::string, double> t2 = {42, "hello", 2.7};t1.swap(t2);  //效果与 std::swap(t1, t2); 一样

​ 一般来说,两个不同类型的元组变量是不可以赋值的,但是如果对应位置的元素类型可以隐式转换,那么赋值是可以接受的,比如:

std::tuple<char, double> t16('A', 2.7);
std::tuple<double, std::string> t17 = t16;  //错误,无法赋值
std::tuple<int, double> t17 = t16; //OK, char 可以隐式转换成 int

如果元组中的元素类型支持通过构造函数隐式转换,赋值也是可以的,请参考 1.1.2 节 FooTest 的例子,这里不再赘述。

2.1.3 tie 和 ignore

​ 除了使用 std::get() 访问元素的元素,还可以使用 std::tie() 方法将元组内的元素与某个具名的变量关联,将元组的内容传递给具名变量。来看个例子:

std::tuple<int, std::string, double> t1(3, "Kitty", 2.7); int age;
std::string name;
double weight;
std::tie(age, name, weight) = t1;std::cout << "Name: " << name << ", Age: " << age << ", Weight: " << weight << " Kg(s)" << std::endl;age = 10;  //修改 age 的值不影响 t1

显然,使用具名变量可以提高代码的可读性,毕竟,一个有具体名字的变量比生冷的 std::get<0> 要强多了。但是需要注意,std::tie() 的捆绑效果是单向的,并且是一次性的,std::tie() 之后再修改具名变量的值不会影响关联的元组的值。

​ 如果关联到的时候对某个元素不感兴趣,可以使用 std::ignore 占位符,比如:

std::tuple<int, std::string, double> t1(3, "Kitty", 2.7); int age;
double weight;
std::tie(age, std::ignore, weight) = t1; //只关心年龄和体重,不关心名字

2.1.4 拼接元组

​ 可以使用 std::tuple_cat() 拼接两个元组变量,得到一个更大的元组,看看这个例子:

std::tuple<int, std::string, double> t1(3, "Kitty", 2.7); auto t2 = std::tuple_cat(t1, std::make_tuple("Garfield", "United Kingdom"));assert(std::tuple_size<decltype(t2)>::value == 5);

拼接后 t2 有五个元素,分别是 (3, “Kitty”, 2.7, “Garfield”, “United Kingdom”)。

2.1.5 std::forward_as_tuple()

​ 2.1.1 节提到了在定义 std::tuple<> 的时候可以使用左值引用类型的元组元素,既然能使用左值引用,当然也可以使用右值引用类型。std::forward_as_tuple() 的作用是返回一个 std::tuple<> 对象,其元素类型是给定的函数参数类型对应的右值引用类型。这句话有点难以理解,用这行代码做例子来理解这个函数:

std::tuple<int&&, FooTest&&> k = std::forward_as_tuple(42, FooTest(5));

当我们传递两个值给 std::forward_as_tuple() 方法时,它的返回值类型是对应的 std::tuple<int&&, FooTest&&>。这个方法存在意义是什么呢?当然是为了参数传递的效率。我们用 std::make_tuple() 跟他做个对比,在对比之前,先看看 FooTest 的实现,我们增加了很多打印信息跟踪这个对象实例的构造和销毁:

struct FooTest {FooTest(const FooTest& f){   std::cout << "FooTest(const FooTest&)" << std::endl; }FooTest(FooTest&& f){   std::cout << "FooTest(FooTest&&)" << std::endl; }FooTest(){   std::cout << "FooTest()" << std::endl; }FooTest(int a){   std::cout << "FooTest(int)" << std::endl; }~FooTest(){   std::cout << "~FooTest()" << std::endl; }
};

先来看看 std::make_tuple() 的执行情况,对于这行代码:

auto kk = std::make_tuple(42, FooTest(5));

打印输出结果如下,执行了两次对象的构造和销毁,其中一次右值构造是因为构造函数返回时产生了一个将亡值临时对象:

FooTest(int)
FooTest(FooTest&&)
~FooTest()
~FooTest()

好了,现在看看 std::forward_as_tuple() 是什么情况,同样的代码:

auto kk = std::forward_as_tuple(42, FooTest(5));

对应的打印结果是:

FooTest(int)
~FooTest()

看到了吗?只在 FooTest(5) 调用时产生了一次 FooTest 对象实例的构造,随后这个对象实例被转发出来,最后随着 kk 销毁的时候一起销毁。现在明白这个方法为什么叫 forward_as_tuple() 了吧?因为它的作用和 std::forward() 类似,具有相同的语意。

​ 由此可见,C++ 对效率的追求到了近乎偏执的地步。类似的右值转发对效率提升是非常显著的,如果有恰当设计的函数配合,右值对象可以“一镜到底”:

void print_Tuple(std::tuple<int&&, FooTest&&> pack)
{ std::cout << std::get<0>(pack) << std::endl; }print_Tuple(std::forward_as_tuple(42, FooTest(5)));

输出结果是:

FooTest(int)
42
~FooTest()

你想到了吗?

2.2 C++ 17 的改进

2.2.1 推断指引

​ std::tuple<> 也支持推断指引,像这样繁琐的代码:

std::tuple<int, std::string, double, std::string, std::string> t10(3, "Kitty", 2.7, "Garfield", "United Kingdom"); 

可以简化为:

std::tuple t1(3, "Kitty", 2.7, "Garfield", "United Kingdom");

你只负责想象,剩下的交给编译器。

2.2.2 结构化绑定

​ 使用 std::tie() 可以将元组内的元素关联到一些具名变量上,提高代码的可读性,但是 std::tie() 的使用并不友好,变量需要提前定义好,写代码很繁琐。C++ 17 引入的结构化绑定语法也适用于 std::tuple<>,使用结构化绑定可以简化代码的实现,2.1.3 节的例子可以这样简单地实现:

std::tuple<int, std::string, double> t1(3, "Kitty", 2.7); auto [age, name, weight] = t1;  //无需事先声明 age, name 和 weight

前面提到过,std::tie() 关联具名变量是单向的一次性动作,结构化绑定虽然也是一次性动作,但是可以通过引用绑定方式修改被关联对象实例的值,比如:

std::tuple<int, std::string, double> t1(3, "Kitty", 2.7); auto& [age, name, weight] = t1; //引用绑定
age = 4;  //同时修改了 t1 的值
assert(std::get<0>(t1) == 4);

除此之外,std::tie() 还有一个局限,那就是它只能用于关联到一个左值类型对象实例,不能用于右值,但是结构化绑定可以,来看一个函数返回值的例子:

std::tuple<int, std::string, double> GetInfo(const std::string& name) {return std::make_tuple(42, "Simon", 108.2);
}int age;
std::string name;
double weight;
std::tie<age, name, weight> = GetInfo("Kitty");  //错误,函数返回值不是左值auto [aa, nn, ww] = GetInfo("Kitty");  //OK,结构化绑定可以

使用结构化绑定的结果就是 aa、nn 和 ww 分别是对应类型的右值引用类型,没有任何临时对象拷贝的开销,非常 nice。

关注作者的算法专栏
https://blog.csdn.net/orbit/category_10400723.html

关注作者的出版物《算法的乐趣(第二版)》
https://www.ituring.com.cn/book/3180

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/66838.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Unity中 Xlua使用整理(二)

1.Xlua的配置应用 xLua所有的配置都支持三种方式&#xff1a;打标签&#xff1b;静态列表&#xff1b;动态列表。配置要求&#xff1a; 列表方式均必须是static的字段/属性 列表方式均必须放到一个static类 建议不用标签方式 建议列表方式配置放Editor目录&#xff08;如果是H…

Python Matplotlib教程-Matplotlib 多子图布局

Python Matplotlib 多子图布局 Matplotlib 是 Python 中最常用的数据可视化库&#xff0c;它提供了强大的功能来绘制不同类型的图表。在实际应用中&#xff0c;通常需要将多个图表绘制在同一个画布上&#xff0c;这就需要用到 多子图布局。本篇文章将详细介绍如何使用 Matplot…

全方位解读消息队列:原理、优势、实例与实践要点

全方位解读消息队列&#xff1a;原理、优势、实例与实践要点 一、消息队列基础认知 在数字化转型浪潮下&#xff0c;分布式系统架构愈发复杂&#xff0c;消息队列成为其中关键一环。不妨把消息队列想象成一个超级“信息驿站”&#xff0c;在古代&#xff0c;各地的信件、物资运…

Photon最新版本PUN 2.29 PREE,在无网的局域网下,无法连接自己搭建的本地服务器

1.图1为官方解答 2.就是加上这一段段代码&#xff1a;PhotonNetwork.NetworkingClient.SerializationProtocol SerializationProtocol.GpBinaryV16; 完美解决 unity 商店最新PUN 2 插件 不能连接 &#xff08;环境为&#xff1a;本地局域网 无外网情况 &#xff09; …

消息中间件类型介绍

消息中间件是一种在分布式系统中用于实现消息传递的软件架构模式。它能够在不同的系统或应用之间异步地传输数据&#xff0c;实现系统的解耦、提高系统的可扩展性和可靠性。以下是几种常见的消息中间件类型及其介绍&#xff1a; 1.RabbitMQ 特点&#xff1a; • 基于AMQP&#…

51单片机(二)中断系统与外部中断实验

中断即单片机因为某些原因E暂定现在的工作P0&#xff0c;转去做其他的工作P1&#xff0c;完了之后继续之前的事P0&#xff0c;其他工作P1就是中断程序&#xff0c;原因E就是中断事件&#xff0c;原因由外部发生&#xff0c;程序不能预测到的是硬中断&#xff0c;可以由程度触发…

python-42-使用selenium-wire爬取微信公众号下的所有文章列表

文章目录 1 seleniumwire1.1 selenium-wire简介1.2 获取请求和响应信息2 操作2.1 自动获取token和cookie和agent2.3 获取所有清单3 异常解决3.1 请求url失败的问题3.2 访问链接不安全的问题4 参考附录1 seleniumwire Selenium WebDriver本身并不直接提供获取HTTP请求头(header…

汽车信息安全 -- S32K1如何更新BOOT_MAC

目录 1.安全启动模式回顾 2.为什么要讨论BOOT_MAC 3.S32K1如何更新? 1.安全启动模式回顾 之前提到过,S32K1系列提供了Crypto Service Engine硬件加密模块(简称CSEc),大家可以通过该芯片系统寄存器SDID.FEATURES(System Device Identification Register)来判断自己的片子…

【Python】Python与C的区别

文章目录 语句结束符代码块表示变量声明函数定义注释格式Python的标识符数据输入input()函数数据输出print()函数 语句结束符 C 语言 C 语言中每条语句必须以分号;结束。例如&#xff0c;int a 10;、printf("Hello, World!");。分号是语句的一部分&#xff0c;用于…

理解Unity脚本编译过程:程序集

https://docs.unity3d.com/Manual/script-compilation.html 关于Unity C#脚本编译的细节&#xff0c;其中一个比较重要的知识点就是如何自定义Assembly。 预定义的assembly 默认情况下&#xff0c;Unity会按照这个规则进行编译。 PhaseAssembly nameScript files1Assembly-…

Linux内核TTY子系统有什么(6)

接前一篇文章&#xff1a;Linux内核TTY子系统有什么&#xff08;5&#xff09; 本文内容参考&#xff1a; Linux TTY子系统框架-CSDN博客 一文彻底讲清Linux tty子系统架构及编程实例-CSDN博客 linux TTY子系统(3) - tty driver_sys tty device driver-CSDN博客 Linux TTY …

《代码随想录》Day31打卡!

《代码随想录》贪心算法&#xff1a;合并区间 本题的完整题目如下所示&#xff1a; 本题的完整思路如下所示&#xff1a; 1.本题依然是先对数组的左边界进行排序。将数组的第一个元素赋值给current。 2.遍历数组&#xff0c;判断current中的右边界和当前元素的左边界是否有重叠…

KL 散度:多维度解读概率分布间的隐秘 “距离”

深入理解KL散度&#xff1a;从多维度全面剖析 损失函数相关文章&#xff08;置顶&#xff09; 1. KL 散度&#xff1a;多维度解读概率分布间的隐秘 “距离” 2. 熵与交叉熵&#xff1a;从不确定性角度理解 KL 散度 3. 机器学习、深度学习关于熵你所需要知道的一切 引言 KL散即…

node-sass@4.14.1报错的最终解决方案分享

输入npm i全安装文件所需的依赖的时候&#xff0c;博主是使用sass去书写的&#xff0c;使用的是node-sass4.14.1和sass-loader7.3.1的版本的&#xff0c;安装的时候老是出现错误&#xff0c; node-sass4.14.1版本不再被支持的原因 node-sass 是一个基于 LibSass 的 Node.js 绑…

设计模式(观察者模式)

设计模式&#xff08;观察者模式&#xff09; 第三章 设计模式之观察者模式 观察者模式介绍 观察者模式&#xff08;Observer Design Pattern&#xff09; 也被称为发布订阅模式 。模式定义&#xff1a;在对象之间定义一个一对多的依赖&#xff0c;当一个对象状态改变的时候…

PyCharm 的安装与使用(Window)

1 PyCharm 简介 PyCharm 是一款由 JetBrains 公司开发的专门用于 Python 语言开发的集成开发环境&#xff08;IDE&#xff09;。以下是其相关介绍&#xff1a; 1.1 特点与功能 智能代码编辑&#xff1a;提供高度智能化的代码编辑器&#xff0c;支持语法高亮、自动补全、代码重…

(vue)el-table-column type=“selection“表格选框怎么根据条件添加禁选

(vue)el-table-column type"selection"表格选框怎么根据条件添加禁选 html <el-table:data"tableData"style"width: 100%"><el-table-columntype"selection"width"55":selectable"checkSelectable">…

C# 之某度协议登录,JS逆向,手机号绑定,获取CK

.NET兼职社区 .NET兼职社区 .NET兼职社区 .NET兼职社区 有需要指导&#xff0c;请私信我留言V或者去社区找客服。

深入Android架构(从线程到AIDL)_20 IPC的Proxy-Stub设计模式02

2、 IBinder接口的一般用途 前言 一般用途 Android的IPC框架仰赖单一的IBinder接口。此时Client端调用IBinder接口的transact()函数&#xff0c;透过IPC机制而调用到远方(Remote)的onTransact()函数。在Java层框架里&#xff0c; IBinder接口实现于Binder基类&#xff0c;如下…