更新时间: 2026-08-27
一个函数想返回两个值,怎么办?C 的做法是搞个结构体,或者传指针让函数往里写。C++ 给了个更轻的答案:std::pair——一个装两个元素的"迷你结构体"。要是想装三个、四个乃至更多,就用 std::tuple。
这篇要回答:pair 和 tuple 到底解决了什么痛点?怎么优雅地把它们拆开?以及 C++17 的结构化绑定是怎么让"打包/解包"这件事彻底变得自然的?
一、为什么需要 pair
先看一个常见场景:map 里存的是键值对,遍历 map 拿到的每一个元素,类型就是 std::pair<const Key, Value>。再看函数返回值:
// 没有 pair 的日子:要么传指针,要么定义结构体
void get_min_max(const std::vector<int>& v, int* min_out, int* max_out);
// 有了 pair:
std::pair<int, int> get_min_max(const std::vector<int>& v);传指针那版有"输出参数"的毛病:调用方得先声明两个变量,再把地址传进去,函数写不写都不确定,可读性差。返回 pair 则一目了然——一个函数返回一个"装了两个数的包"。
pair 的构造也很直接:
#include <utility>
std::pair<int, double> p1(42, 3.14); // 直接构造
std::pair<int, double> p2{42, 3.14}; // 列表初始化
auto p3 = std::make_pair(42, 3.14); // 类型自动推导(C++11 起可用 {} 替代)
std::pair<int, double> p4 = {42, 3.14}; // C++11 起可直接赋值
// 访问成员:.first 和 .second
std::cout << p1.first << " " << p1.second;成员就叫 first、second,朴实无华。make_pair 在 C++11 之后其实可以不用了——写 std::pair{42, 3.14} 就能推导,但老代码里 make_pair 依然常见,要认得。
二、tuple:超出两个值
两个值用 pair,三个及以上就要 tuple 了。tuple 是"定长异构列表"——长度编译期固定,每个位置的类型可以不同:
#include <tuple>
// 三个不同类型
std::tuple<int, std::string, double> t(1, "hello", 3.14);
// 访问:std::get<下标>(t)
std::cout << std::get<0>(t); // 1
std::cout << std::get<1>(t); // hello
std::cout << std::get<2>(t); // 3.14
// 也可以按类型取(类型必须唯一,否则歧义)
std::string s = std::get<std::string>(t);注意 get 的模板参数是编译期常量——不能写 std::get<runtime_index>(t),下标必须写死。想按运行时下标取,得用 std::get + if constexpr(那是第 48 篇之后的话题了)。
其实 pair 就是 tuple 的特例:std::pair<int, double> 和 std::tuple<int, double> 差不多。标准库甚至把 pair 设计得可以和 tuple 互相转换:
std::tuple<int, double> t = std::make_tuple(1, 3.14);
std::pair<int, double> p = t; // tuple → pair(两个元素时)三、C 对照:结构体 vs pair/tuple
C 里打包多个返回值,标准做法是定义一个结构体:
typedef struct {
int code;
double value;
} Result;
Result get_result(void) {
Result r = { 0, 0.0 };
// ...
return r;
}| 需求 | C | C++ |
|---|---|---|
| 返回两个值 | 定义结构体 / 输出参数 | std::pair |
| 返回 N 个值 | 定义结构体 | std::tuple |
| 匿名临时打包 | 不支持(必须定义类型) | std::make_pair / make_tuple |
| 解包 | r.code 逐个取 | 结构化绑定一行拆开 |
C 的结构体适合"这个组合会被反复使用、有明确业务含义"的场景(比如学生信息、错误码+信息);pair/tuple 适合"一次性打包,用完就拆"的场景。业务含义强的建议还是定义结构体/类——.first、.second、.get<0>() 可读性不如 r.code、r.name。
四、结构化绑定:C++17 的解包神器
打包容易,拆包呢?C++11/14 时代拆 tuple 很痛苦:
// C++11/14 时代的痛苦拆法
int code;
double value;
std::string msg;
std::tie(code, value, msg) = get_result(); // 必须预先声明三个变量std::tie 按顺序把 tuple 的元素绑定到变量上,但变量得先声明,代码绕。C++17 的结构化绑定直接一行搞定:
// C++17
auto [code, value, msg] = get_result(); // 一次性声明并绑定
// map 遍历的经典写法
for (const auto& [key, value] : my_map) {
std::cout << key << " -> " << value << "\n";
}auto [a, b, c] = ... 让编译器帮你"拆包",等号右边的 pair/tuple/结构体/数组都可以。注意几个细节:
auto [a, b] = p;是拷贝。想拿引用,写auto& [a, b] = p;或const auto& [a, b] = p;- 拆结构体也行——成员按声明顺序绑定(C++17 要求所有成员都是非静态、公开的,C++20 起支持匿名结构体成员等更多情况)
- 循环里用结构化绑定特别顺手,上面 map 遍历就是标准写法
@startuml
left to right direction
skinparam nodeFontSize 13
skinparam backgroundColor #FFFFFF
rectangle "std::pair<int, std::string> p" as pkg #E8F1FF {
node "first: 42" as f #DCE9FF
node "second: \"cpp\"" as s #DCE9FF
}
rectangle "auto [a, b] = p;" as unpack #FFF3D6 {
node "a = 42" as a #FFF3D6
node "b = \"cpp\"" as b #FFF3D6
}
pkg --> unpack : "结构化绑定拆包"
@enduml结构体绑定要求成员都是公开的,这跟 pair/tuple 天然匹配(它们成员就是公开的 first/second)。自己写的类想支持结构化绑定,最简单的方式是别用私有成员藏数据,或者实现 get<N>() 特化——后者属于进阶话题,先不展开。
五、C++17 之前的老写法:std::tie
虽然 C++17 已经是事实标准(本站统一用 C++17),但老代码里 std::tie 还是会出现,认得它有助于读旧项目:
#include <tuple>
int code;
std::string msg;
std::tie(code, msg) = get_result(); // 先声明,再绑定
// tie 还有个大用途:字典序比较
auto [a, b] = std::pair{1, 2};
std::tie(a, b) < std::tie(c, d) // 先比 a 和 c,再比 b 和 dpair/tuple 本身就自带 ==、< 这些比较运算符,比较规则是"逐元素比较",所以两个 pair 直接比大小就行,一般用不上 tie。但 tie 可以让普通变量也获得这种比较能力,这个技巧在排序自定义比较器里偶尔见到。
六、pair 在 map 里的存在感
map 的 insert、迭代器解引用拿到的都是 pair,所以这里多说一句:
#include <map>
std::map<std::string, int> scores;
scores.insert({"alice", 90}); // 构造的是 pair<const string, int>
for (const auto& [name, score] : scores) { // 结构化绑定解 pair
std::cout << name << ": " << score << "\n";
}注意 key 类型是 const std::string——map 不允许修改键(改了会破坏排序),所以 pair 的第一元素带 const。这跟 36 篇 map 里讲的"键不可变"是一致的。
七、与本站主线衔接
map/set的插入返回值和迭代器都用 pair 承载(insert返回pair<iterator, bool>),36 篇讲过- 算法库
std::minmax、std::partition_point等会返回 pair 结果,39 篇讲过 std::optional(51 篇)也经常和 pair 一起出现在错误处理里- 结构化绑定的底层是编译期解包,理解它有助于后面读 move 语义(
auto [a, b] = std::move(p);的边界情况)
八、一句话总结
pair 装两个、tuple 装任意多个,是轻量的"打包工具",配合 C++17 结构化绑定 auto [a, b] = ... 一行拆包,让"函数返回多个值""遍历键值对"这些日常操作变得自然流畅;业务含义强的组合仍然优先定义结构体/类,pair/tuple 适合一次性打包的场景。