C++ 的 pair 和 tuple

server/2025/1/11 14:40:16/

pair_0">1 std::pair

pair_1">1.1 C++ 98 的 std::pair

pair__2">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 的两个参数中推断出它们的类型,所以就不需要显示指定具体的类型了。推断指引是个好东西,能少敲几次键盘,节省体力。

tuple_133">2 std::tuple

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

tuple__137">2.1 std::tuple 的语法

tuple__138">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”)。

tuple_268">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.ppmy.cn/server/157494.html

相关文章

SQLite PRAGMA

SQLite的PRAGMA命令是一种特殊的命令&#xff0c;用于在SQLite环境中控制各种环境变量和状态标志。PRAGMA值可以被读取&#xff0c;也可以根据需求进行设置【0†source】。 PRAGMA命令的语法格式如下&#xff1a; 要查询当前的PRAGMA值&#xff0c;只需提供该PRAGMA的名字&am…

USB基础 -- USB 控制传输(Control Transfer)的重传机制

USB 控制传输&#xff08;Control Transfer&#xff09;的重传机制 1. 控制传输的事务结构 控制传输分为三个阶段&#xff0c;每个阶段都有自己的事务&#xff0c;并可能触发重传机制&#xff1a; 设置阶段&#xff08;Setup Stage&#xff09;&#xff1a;主机发送 8 字节的…

初识verilog HDL

为什么选择用Verilog HDL开发FPGA&#xff1f;&#xff1f;&#xff1f; 硬件描述语言&#xff08;Hardware Descriptipon Lagnuage&#xff0c;HDL&#xff09;通过硬件的方式来产生与之对应的真实的硬件电路&#xff0c;最终实现所设计的预期功能&#xff0c;其设计方法与软件…

kubeneters-循序渐进Cilium网络(二)

文章目录 概要IP 地址配置接口配置解析结论 概要 接续前一章节&#xff0c;我们还是以这张图继续深入Cilium网络世界 IP 地址配置 通过检查 Kubernetes 集群的当前环境&#xff0c;可以获取实际的 IP 地址和配置信息。这些信息将被补充到之前的网络示意图中&#xff0c;以使…

awr报告无法生成:常见案例与解决办法

awr报告无法生成:常见案例与解决办法 STATISTICS_LEVEL设置过低数据库打开状态不对主库隐含参数设置错误MMON子进程被SuspendSYS模式统计信息过期WRH$_SQL_PLAN表数据量太大AWR绑定变量信息收集超时撞上数据库Bug 9040676STATISTICS_LEVEL设置过低 STATISTICS_LEVEL设置为BAS…

计算机网络之---子网划分与IP地址

子网划分与IP地址的关系 在计算机网络中&#xff0c;子网划分&#xff08;Subnetworking&#xff09;是将一个网络划分为多个子网络的过程。通过子网划分&#xff0c;可以有效地管理和利用IP地址空间&#xff0c;提高网络的性能、安全性和管理效率。 子网划分的基本目的是通过…

现代谱估计的原理及MATLAB仿真(二)(AR模型法、MVDR法、MUSIC法)

现代谱估计的原理及MATLAB仿真AR参数模型法&#xff08;参数模型功率谱估计&#xff09;、MVDR法&#xff08;最小方差无失真响应法&#xff09;、MUSIC法&#xff08;多重信号分类法&#xff09; 文章目录 前言一、AR参数模型1 原理2 MATLAB仿真 二、MVDR法1 原理2 MATLAB仿真…

Ubuntu 安装 Java 1.8

如果你希望使用 Oracle JDK 8&#xff0c;可以按照以下步骤操作&#xff1a; 下载 Oracle JDK 8&#xff1a; 访问 Oracle 官方网站 下载适用于 Ubuntu 的 JDK 8 版本 安装 Oracle JDK 8&#xff1a; 将下载的 JDK 8 压缩包解压到一个目录中&#xff0c;例如 /opt/module&…