15. std::pair 与 std::tuple 详解:结构化绑定、元组的使用、与自定义类型的对比
好,咱们今天来聊聊 C++ 里两个非常实用的“小容器”——std::pair 和 std::tuple。
说实话,我早年写 C++ 的时候,经常为了“返回两个值”这种事纠结。要么定义个结构体,要么传引用参数,总觉得不够优雅。后来 STL 里有了 pair,再后来 C++11 引入了 tuple,C++17 又带来了结构化绑定……嗯,写代码的感觉一下子就顺了。
这一章,我就带你把这些东西彻底捋清楚。
15.1 std::pair:两个值的“黄金搭档”
std::pair 定义在 头文件中。它很简单,就是把两个值打包在一起。两个值的类型可以不同。
我个人习惯把它用在“返回两个关联数据”的场景。比如,查找一个元素,找到了就返回它的迭代器和状态。
15.1.1 基本用法
#include <iostream>
#include <utility>
#include <string>
int main() {
// 直接构造
std::pair<int, std::string> p1(42, "hello");
// 用 make_pair 推导类型(C++11 起)
auto p2 = std::make_pair(3.14, "world");
// 访问成员
std::cout << p1.first << " " << p1.second << std::endl;
return 0;
}
你看,first 和 second 就是它的两个成员。命名很直白,没什么花哨的。
15.1.2 我在项目中遇到的“pair 陷阱”
我曾经在维护一个老项目时,看到有人用 std::pair<int, int> 来表示一个二维坐标。代码里到处都是 .first 和 .second,读起来非常痛苦。
你想想看,point.first 到底是 x 还是 y?没人知道。除非你每次都去查文档或者看注释。
first 和 second 的含义不直观时,别用 pair。定义一个简单的结构体,给成员起个好名字,比什么都强。
15.2 std::tuple:超过两个值的“万能包裹”
如果说 pair 是双人自行车,那 tuple 就是一辆小巴车——它能装任意多个不同类型的值。
std::tuple 定义在 头文件中。C++11 引入,C++17 和 C++20 又给它加了不少新特性。
15.2.1 创建和访问 tuple
#include <iostream>
#include <tuple>
#include <string>
int main() {
// 直接构造
std::tuple<int, double, std::string> t1(42, 3.14, "tuple");
// 用 make_tuple 推导类型
auto t2 = std::make_tuple(100, 2.71, "hello");
// 用 std::get 访问(按索引)
std::cout << std::get<0>(t1) << " "; // 42
std::cout << std::get<1>(t1) << " "; // 3.14
std::cout << std::get<2>(t1) << std::endl; // "tuple"
// 也可以按类型访问(前提是类型唯一)
std::cout << std::get<double>(t1) << std::endl; // 3.14
return 0;
}
这里有个细节:std::get 的模板参数可以是索引,也可以是类型。但按类型访问时,如果 tuple 里有多个相同类型,编译器会报错。
15.2.2 解包 tuple:std::tie 和 std::apply
在 C++17 结构化绑定出现之前,我们主要靠 std::tie 来解包 tuple。
#include <iostream>
#include <tuple>
#include <string>
int main() {
auto t = std::make_tuple(42, 3.14, "hello");
int a;
double b;
std::string c;
// 用 tie 解包
std::tie(a, b, c) = t;
std::cout << a << " " << b << " " << c << std::endl;
// 如果某个值不想要,可以用 std::ignore 占位
std::tie(a, std::ignore, c) = t;
return 0;
}
std::apply 是 C++17 引入的,它可以把 tuple 展开后作为参数传给一个可调用对象。
#include <iostream>
#include <tuple>
void print(int a, double b, const std::string& c) {
std::cout << a << " " << b << " " << c << std::endl;
}
int main() {
auto t = std::make_tuple(42, 3.14, "hello");
std::apply(print, t); // 等价于 print(42, 3.14, "hello")
return 0;
}
这个功能在泛型编程里特别有用。我曾在写一个反射库时,用 std::apply 把从配置文件里读到的参数列表直接传给函数,省了不少事。
15.3 结构化绑定:C++17 的“语法糖”
好了,重点来了。结构化绑定(Structured Binding)是 C++17 最实用的特性之一。它让你可以把 pair、tuple、甚至数组和结构体的成员直接“拆”成独立的变量。
15.3.1 基本语法
#include <iostream>
#include <tuple>
#include <string>
std::pair<int, std::string> getData() {
return {42, "hello"};
}
int main() {
// 结构化绑定 pair
auto [id, name] = getData();
std::cout << id << " " << name << std::endl;
// 结构化绑定 tuple
auto t = std::make_tuple(1, 2.5, "world");
auto [x, y, z] = t;
std::cout << x << " " << y << " " << z << std::endl;
// 结构化绑定数组
int arr[] = {10, 20, 30};
auto [a, b, c] = arr;
std::cout << a << " " << b << " " << c << std::endl;
return 0;
}
你看,代码一下子清爽了很多。不用再写 .first、.second,也不用 std::get<0> 了。
15.3.2 修饰符:const、引用、移动语义
结构化绑定也支持各种修饰符,这点很重要。
#include <iostream>
#include <tuple>
#include <string>
int main() {
auto t = std::make_tuple(42, 3.14, "hello");
// 绑定到引用,可以修改原 tuple
auto& [r1, r2, r3] = t;
r1 = 100;
std::cout << std::get<0>(t) << std::endl; // 100
// const 引用,只读
const auto& [cr1, cr2, cr3] = t;
// cr1 = 200; // 编译错误
// 移动语义
auto&& [mr1, mr2, mr3] = std::move(t);
return 0;
}
我个人建议:如果你只是想读取值,用 const auto& 最安全。如果要修改原数据,用 auto&。默认的 auto 会拷贝一份,对于大对象要注意性能。
15.3.3 结构化绑定与自定义类型
结构化绑定不只对 STL 类型有效。只要你的自定义类型满足一定条件,也能用。
对于聚合类型(比如简单的结构体),直接就能用:
#include <iostream>
#include <string>
struct Point {
int x;
int y;
std::string name;
};
int main() {
Point p{10, 20, "origin"};
auto [px, py, pname] = p;
std::cout << px << " " << py << " " << pname << std::endl;
return 0;
}
对于非聚合类型,需要提供 std::tuple_size、std::tuple_element 的特化,以及 get 函数。嗯,这个有点复杂,一般用不到,我就不展开讲了。
15.4 pair vs tuple vs 自定义类型:怎么选?
这是个很实际的问题。我见过不少人在选择时纠结。下面这张表是我个人的经验总结:
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 只有两个值,含义明确(如 key-value) | std::pair |
简单、高效、STL 容器广泛使用 |
| 两个值,但含义不直观(如坐标) | 自定义结构体 | 可读性更好,.x 比 .first 清晰 |
| 三个及以上值,临时组合 | std::tuple |
灵活,无需定义新类型 |
| 三个及以上值,在多个地方使用 | 自定义结构体 | 可维护性更好,修改方便 |
| 函数返回多个值 | std::tuple + 结构化绑定 |
代码简洁,调用方一目了然 |
| 需要按类型访问元素 | std::tuple |
std::get<Type> 很方便 |
说白了,选择的标准就两条:可读性和使用频率。
临时用一次,用 tuple 没问题。如果这个组合在代码里反复出现,我建议你定义一个结构体。别偷懒,未来的你会感谢现在的你。
15.5 知识体系总览
下面这张图帮你梳理了本章的核心知识点:
15.6 实战建议与避坑
最后,分享几个我踩过的坑和总结的经验:
std::map<int, std::string> m = {{1, "one"}, {2, "two"}};
for (const auto& [key, value] : m) {
std::cout << key << ": " << value << std::endl;
}
std::tuple<int, int, int> 表示 RGB 颜色。结果后来需求变更,要加一个 alpha 通道。我不得不把所有用到这个 tuple 的地方都改一遍。如果一开始就定义个 struct Color { int r, g, b; };,加个 alpha 字段就简单多了。
嗯,总结一下:pair 适合简单的 key-value,tuple 适合临时组合,结构化绑定让代码更清爽。但别忘了,当数据有明确的语义时,自定义结构体永远是最清晰的选择。
希望这些经验对你有帮助。写代码嘛,舒服最重要。
公众号:蓝海资料掘金营,微信 deep3321