14. pair与tuple:从双人到多人的数据打包术
说实话,pair和tuple这俩东西,在我刚学C++那会儿觉得挺鸡肋的。不就是把几个值捆在一起吗?我自己写个struct不香吗?后来真上了项目,才发现——嗯,有时候你就是要一个轻量级的“临时组合”,专门给std::map、std::unordered_map或者某些算法返回值用的。这时候再专门写个struct,反而显得啰嗦。
好,咱们今天就把pair和tuple彻底聊透。从构造到使用,再到C++17的结构化绑定,一条线串下来。
14.1 pair:最简单的数据对
std::pair定义在<utility>头文件里。它就是把两个值打包成一个对象。两个值的类型可以不同,也可以相同。
14.1.1 pair的构造
构造pair的方式有好几种,我个人最常用的是std::make_pair,因为它省得你写模板参数。
#include <iostream>
#include <utility> // pair
#include <string>
int main() {
// 方式1:直接构造
std::pair<int, std::string> p1(42, "hello");
// 方式2:make_pair(推荐,类型自动推导)
auto p2 = std::make_pair(3.14, "world");
// 方式3:拷贝构造
std::pair<int, std::string> p3(p1);
// 方式4:C++11 列表初始化
std::pair<int, std::string> p4{100, "C++"};
std::cout << p1.first << ", " << p1.second << std::endl;
return 0;
}
这里有个小细节:make_pair推导出来的类型有时候会带引用或const,如果你想要精确控制,还是显式写模板参数更稳妥。我在项目中遇到过一个问题——用make_pair传了一个字符串字面量,结果推导出const char*而不是std::string,后面赋值时出了点小麻烦。从那以后,我对于需要长期持有的pair,都会显式指定类型。
14.1.2 pair的访问与比较
访问pair的两个成员,用.first和.second。比较操作符也是现成的:先比较first,再比较second。这个顺序很重要,你想想看,map的底层就是用pair的operator<来排序的。
std::pair<int, int> a(1, 2);
std::pair<int, int> b(1, 3);
std::pair<int, int> c(2, 1);
if (a < b) // true,因为first相等,second: 2 < 3
std::cout << "a < b" << std::endl;
if (a < c) // true,因为first: 1 < 2
std::cout << "a < c" << std::endl;
小提示:pair的operator==要求两个成员都支持==比较。如果你的类型没有定义==,那就没法直接用pair的相等比较了。
14.2 tuple:pair的升级版
pair只能装两个值。那三个、四个、五个呢?C++11引入了std::tuple,定义在<tuple>头文件里。说白了,tuple就是pair的泛化版本,可以装任意数量的值。
14.2.1 tuple的构造与访问
构造tuple的方式和pair类似。但访问成员就不能用.first、.second了,得用std::get。
#include <tuple>
#include <iostream>
#include <string>
int main() {
// 方式1:直接构造
std::tuple<int, double, std::string> t1(1, 2.5, "tuple");
// 方式2:make_tuple(推荐)
auto t2 = std::make_tuple(10, 3.14, "hello");
// 方式3:std::tie 构造(绑定到已有变量)
int x = 0;
double y = 0.0;
std::string z;
std::tie(x, y, z) = std::make_tuple(100, 2.71, "tie");
// 访问:按索引
std::cout << std::get<0>(t1) << std::endl; // 1
std::cout << std::get<1>(t1) << std::endl; // 2.5
std::cout << std::get<2>(t1) << std::endl; // "tuple"
// 访问:按类型(要求类型唯一)
std::cout << std::get<double>(t1) << std::endl; // 2.5
return 0;
}
注意:按类型访问时,如果tuple里有多个相同类型,编译会报错。比如tuple<int, int, double>,你用get<int>就歧义了。这时候只能用索引。
14.2.2 tuple的常用操作
| 操作 | 说明 | 示例 |
|---|---|---|
std::get<I>(t) | 获取第I个元素(I是编译期常量) | get<0>(t) |
std::tuple_size<T>::value | 获取tuple的元素个数 | tuple_size<decltype(t)>::value |
std::tuple_element<I, T>::type | 获取第I个元素的类型 | tuple_element<0, decltype(t)>::type |
std::tie(t1, t2, ...) | 创建左值引用的tuple,用于解包 | tie(a, b) = t |
std::tuple_cat(t1, t2) | 拼接两个tuple | auto t3 = tuple_cat(t1, t2) |
我记得有一次写一个多返回值函数,返回了一个4元tuple。调用方用std::tie一口气解包到四个变量里,代码看起来特别清爽。不过后来代码维护的人说看不懂……所以我的建议是:tuple适合内部使用,如果是公开接口,还是用struct更清晰。
14.3 结构化绑定(C++17):告别冗长的解包
在C++17之前,从pair或tuple里取出值,要么用.first/.second,要么用std::get,要么用std::tie。都挺啰嗦的。C++17的结构化绑定(Structured Bindings)直接让这件事变得优雅。
14.3.1 基本语法
#include <tuple>
#include <map>
#include <iostream>
#include <string>
int main() {
// 绑定pair
std::pair<int, std::string> p{42, "answer"};
auto [id, name] = p; // id = 42, name = "answer"
std::cout << id << ", " << name << std::endl;
// 绑定tuple
std::tuple<int, double, char> t{1, 2.5, 'A'};
auto [a, b, c] = t; // a=1, b=2.5, c='A'
// 绑定数组
int arr[3] = {10, 20, 30};
auto [x, y, z] = arr; // x=10, y=20, z=30
// 绑定map迭代器(经典用法)
std::map<int, std::string> m = {{1, "one"}, {2, "two"}};
for (const auto& [key, value] : m) {
std::cout << key << ": " << value << std::endl;
}
return 0;
}
你看,auto [key, value] 这一行,直接把pair的两个成员拆开命名了。不用再写it->first、it->second,代码可读性提升一大截。
14.3.2 修饰符与引用绑定
结构化绑定支持const、&、&&等修饰符。这个细节很重要——如果你想要修改原pair里的值,或者避免拷贝,一定要加引用。
std::pair<int, std::string> p{1, "hello"};
// 拷贝绑定(修改不影响原pair)
auto [a, b] = p;
a = 100; // p.first 还是 1
// 引用绑定(修改影响原pair)
auto& [c, d] = p;
c = 200; // p.first 变成 200
// const引用绑定(只读)
const auto& [e, f] = p;
// e = 300; // 编译错误
注意:结构化绑定不能直接用于std::tuple的右值引用绑定(比如auto&& [a, b] = std::make_tuple(...)),因为右值tuple的成员是临时对象,引用绑定会导致悬垂引用。我见过有人踩过这个坑——绑定了一个临时tuple,然后访问时数据已经销毁了。
14.3.3 结构化绑定的底层原理
结构化绑定并不是创建了一个新的变量,而是编译器帮你生成了若干个“别名”。对于pair和tuple,它实际上调用了std::get<I>。对于数组,它直接访问下标。对于结构体,它访问公有成员。
这意味着:结构化绑定对性能没有额外开销。它就是个语法糖,编译后和你手写get<0>、get<1>是一样的。
14.4 知识体系图
下面这张图把pair、tuple和结构化绑定的关系梳理清楚了。你可以看到,pair是tuple的特例(2元tuple),而结构化绑定是它们统一的解包方式。
14.5 避坑指南与个人经验
最后,我分享几个实际项目中遇到的坑,希望能帮你少走弯路。
- pair的隐式转换:我曾经用
make_pair("key", 123)往map里插数据,结果key的类型是const char*而不是std::string。虽然map的key是string,但pair构造时不会自动转换,导致多了一次拷贝。后来我改成std::pair<std::string, int>("key", 123),问题解决。 - tuple的get索引必须是编译期常量:你不能写
for(int i=0; i<3; ++i) get<i>(t)。i是运行期变量,编译不过。如果你需要遍历tuple,得用模板元编程或者C++17的std::apply。 - 结构化绑定与auto&&的陷阱:前面提过,绑定临时对象时用
auto&&会导致悬垂引用。我的建议是:除非你确定绑定的对象是左值且生命周期足够长,否则一律用auto或const auto&。 - 不要滥用tuple:tuple虽然方便,但成员没有名字。代码里到处都是
get<0>、get<1>,三个月后你自己都看不懂哪个是哪个。我个人的原则是:超过3个成员,或者成员语义不明确,就写成struct。
核心总结:
- pair装2个值,tuple装任意多个值
- 结构化绑定是C++17最实用的语法糖之一,让pair/tuple的解包变得优雅
- 引用绑定可以避免拷贝,但要小心生命周期
- tuple适合内部临时使用,公开接口建议用struct
公众号:蓝海资料掘金营,微信deep3321