两个值,一个对象
有时你需要把两样东西放在一起:名字和分数,x 和 y,“是否成功”的标志和结果。你可以为每一种组合都定义一个 struct,但对于临时用一下的组合来说,这样太繁琐了。std::pair(位于 <utility>)把恰好两个值打包成一个对象,std::tuple(位于 <tuple>)则把它推广到任意固定数量的值。
其实你之前已经间接见过 std::pair 了:std::map 的每个元素都是一个 pair<const Key, Value>。本页会把它讲清楚,并展示构建和拆解这些类型的现代、易读的写法。
这两个成员总是叫 .first 和 .second,不会采用你的变量名。这是通用类型的代价:字段名只表示位置,不表达含义。
创建 pair
创建 pair 有三种常见方式,它们得到的对象完全一样。
make_pair 会为你推导元素类型,这在 C++17 之前很方便。如今花括号初始化加上类模板实参推导(pair p{"Boris", 85};)已经能覆盖大多数情况,但在现有代码中你仍然会到处看到 make_pair。
类型推导有一个陷阱:make_pair("hi", 3) 推导出的是 pair<const char*, int>,而不是 pair<string, int>。字符串字面量不是 std::string。如果你需要 string,就明确写出来,比如 make_pair(string("hi"), 3),或者写明 pair 的类型,否则之后的比较或复制可能会出乎你的意料。
用结构化绑定拆包
到处读写 .first 和 .second,代码很快就会变得难以阅读,因为这些名字什么也说明不了。C++17 的结构化绑定让你只用一行就能给两个字段起真正的名字:
在遍历 map 的基于范围的 for 循环中,它的优势尤其明显,因为每个元素都是一个 pair。不用再写 it->first / it->second,直接给键和值命名即可:
在循环中使用 const auto&,就像对待任何容器元素一样:它避免了复制每个 pair,也表明你只是在读取。去掉 & 就会复制每一个条目,在大的 map 上这是一个不易察觉的性能 bug。
两个不够用时:tuple
pair 最多只能放两个值。需要三个或更多值时,std::tuple 是同样的思路,只是数量任意。用花括号初始化或 make_tuple 创建它,用 std::get<N> 读取,其中 N 是一个编译期下标。
get<> 中的下标必须是编译时已知的常量。如果 i 是运行时变量,get<i>(record) 就无法编译:tuple 的各个字段可以是不同的类型,所以元素类型必须在编译期间确定,而不是在运行时。如果你发现自己想要一个运行时下标,那你需要的很可能是 vector。
结构化绑定同样适用于 tuple,这也是读取 tuple 时最易读的写法:
返回多个值
日常使用这些类型最常见的原因,是从函数中返回多个值,而不必专门定义一个结构体,或者摆弄输出参数。把结果打包进 pair 或 tuple,在调用处拆包即可。
如果有三个或更多结果,用同样的方式返回一个 tuple。还有一个较老的技巧 std::tie,它把值拆包到已有的变量中,而不是声明新变量;当你想用 std::ignore 忽略某个字段时,它很有用:
不过也要适可而止:如果同一组字段出现在不止一个地方,或者你总是记不清 .second 到底是分数还是数量,就定义一个带具名成员的 struct。pair 和 tuple 最适合局部的、短暂的组合;一旦数据的生命周期超过一个表达式,具名字段就更胜一筹。
比较与排序
还有一个方便之处:pair 和 tuple 自带按字典序工作的比较运算符:先比较第一个元素,只有在第一个元素相等时才比较下一个。这让它们非常适合用作排序的键。
注意字段的顺序很重要:把 age 放在前面,就会先按年龄排序,年龄相同时再按名字排序。如果想先按名字排序,就要交换元素的顺序。正是因为有这种默认比较,pair<priority, item> 才成为优先队列中的常见写法。
下一步:迭代器
你已经看到 .first、.second、it->first 和 *it 在容器周围频繁出现,而真正把一个 pair 元素和它所在的 map 联系起来的,是迭代器。下一页会好好讲解迭代器:begin() 和 end() 到底返回什么,++it 如何遍历容器,以及迭代器失效这个会导致 C++ 中某些最棘手的未定义行为的陷阱。
常见问题
C++ 中 pair 和 tuple 有什么区别?
std::pair 只能保存两个值,通过 .first 和 .second 访问。std::tuple 可以保存任意固定数量的值(零个、两个、三个或更多),通过 std::get<N>(t) 访问。pair 本质上就是一个成员名更友好的二元 tuple;只有在需要三个或更多字段时才用 tuple。
C++ 中如何访问 tuple 的元素?
使用带编译期下标的 std::get<N>(t),例如 std::get<0>(t)。从 C++17 开始,你还可以用结构化绑定来拆包:auto [a, b, c] = t; 会为每个元素提供一个具名变量。不能用运行时变量作为 tuple 的下标:std::get<i> 要求 i 是常量。
C++ 中如何从函数返回多个值?
返回一个 std::pair 或 std::tuple,然后在调用处用结构化绑定拆包:auto [ok, value] = parse(text);。这比输出参数更简洁,也不必专门定义一个一次性的结构体;不过当这些字段不止在一次调用中使用时,具名的结构体可读性更好。