16、pair与tuple:pair的构造与使用、make_pair、tuple(C++11)、结构化绑定(C++17)、实战:多值返回函数
说实话,C++里处理「多个值」这件事,早期挺麻烦的。你想返回两个数?要么定义个结构体,要么用输出参数传引用。直到STL给了我们pair,后来又有了tuple,这事儿才算利索了。
我个人习惯是:只要不超过两个值,优先用pair;超过两个,直接上tuple。C++17之后有了结构化绑定,代码读起来就像在写Python一样舒服。嗯,咱们今天就把这块彻底讲透。
pair:最基础的二元组
pair定义在<utility>里,说白了就是把两个值捆在一起。它的模板参数可以指定两个类型,这两个类型可以完全不同。
#include <iostream>
#include <utility>
#include <string>
int main() {
// 直接构造
std::pair<int, std::string> p1(42, "hello");
// 用make_pair,类型自动推导
auto p2 = std::make_pair(3.14, "world");
// 访问成员
std::cout << p1.first << " " << p1.second << std::endl;
std::cout << p2.first << " " << p2.second << std::endl;
return 0;
}
这里有个细节:make_pair在C++11之后基本可以替代手写构造。为什么?因为它会自动推导类型,你少写一堆模板参数。我在项目中见过有人手写pair<string, int>写了十几遍,其实一个make_pair就搞定了。
小技巧:如果你用C++17及以上,pair的模板参数可以省略,编译器会从构造函数参数推导:std::pair p(1, 2.0); 这写法简洁多了。
pair的比较与赋值
pair支持比较操作,规则是先比较first,再比较second。这个特性在排序时特别有用。
std::pair<int, int> a(1, 2);
std::pair<int, int> b(1, 3);
std::pair<int, int> c(2, 1);
// a < b 为 true,因为 first 相等,second 2 < 3
// a < c 为 true,因为 first 1 < 2
赋值也很直接:
auto p = std::make_pair(10, "test");
p.first = 20; // 修改第一个元素
p.second = "changed"; // 修改第二个元素
注意:pair的成员first和second是公开的,可以直接修改。但如果你用const pair,那就不能改了。我曾经在代码里忘了加const,结果一个不该被修改的pair被意外改了值,排查了半天……
tuple:C++11带来的多值容器
当你要捆三个以上的值时,pair就不够用了。这时候tuple登场。它定义在<tuple>里,可以装任意多个任意类型的值。
#include <tuple>
#include <iostream>
#include <string>
int main() {
// 构造一个三元组
std::tuple<int, double, std::string> t1(1, 3.14, "tuple");
// 用make_tuple自动推导
auto t2 = std::make_tuple(100, 2.718, "hello", 'A');
// 用std::get访问元素
std::cout << std::get<0>(t1) << std::endl; // 按索引访问
std::cout << std::get<double>(t1) << std::endl; // 按类型访问(类型必须唯一)
return 0;
}
你想想看,std::get有两种用法:按索引和按类型。按类型访问有个前提——这个类型在tuple里只能出现一次,否则编译器会报错。我记得有次在代码里放了两个int,然后试图用get<int>,编译器直接甩了个ambiguous error给我。
tuple的常用操作
| 操作 | 说明 | 示例 |
|---|---|---|
std::get<N>(t) |
按索引获取第N个元素 | std::get<0>(t) |
std::get<T>(t) |
按类型获取元素 | std::get<int>(t) |
std::tuple_size<T>::value |
获取tuple的元素个数 | std::tuple_size<decltype(t)>::value |
std::tuple_element<N, T>::type |
获取第N个元素的类型 | std::tuple_element<0, decltype(t)>::type |
std::tie |
将tuple解包到多个变量 | int x; double y; std::tie(x, y) = t; |
std::tie是个好东西,它创建了一个引用的tuple,可以一次性把值拆出来。不过C++17之后,我们有更好的选择——结构化绑定。
结构化绑定:C++17的语法糖
结构化绑定(Structured Binding)让解包变得极其优雅。你不需要写std::get,也不需要std::tie,直接用auto [a, b, c]就能把tuple或pair拆开。
#include <tuple>
#include <iostream>
#include <string>
std::tuple<int, double, std::string> getData() {
return {42, 3.14, "answer"};
}
int main() {
// 结构化绑定
auto [id, value, name] = getData();
std::cout << id << " " << value << " " << name << std::endl;
// 也可以用于pair
std::pair p("key", 100);
auto [k, v] = p;
std::cout << k << " " << v << std::endl;
return 0;
}
结构化绑定不仅适用于tuple和pair,还适用于数组和结构体。说白了,只要你的类型支持std::tuple_size和std::get,就能用这个语法。
核心要点:结构化绑定是C++17最重要的语法糖之一。它让多值返回的代码可读性大幅提升。我个人现在写C++17代码时,几乎不用std::tie了,全部换成结构化绑定。
实战:多值返回函数
咱们来看一个实际场景。假设你要写一个函数,解析一个字符串,返回解析结果和状态码。传统做法是传引用参数,现在用tuple或pair就清爽多了。
#include <tuple>
#include <string>
#include <iostream>
#include <optional>
// 返回解析结果和状态
std::tuple<bool, int, std::string> parseValue(const std::string& input) {
try {
int val = std::stoi(input);
return {true, val, "success"};
} catch (...) {
return {false, 0, "parse error"};
}
}
int main() {
auto [ok, value, msg] = parseValue("42");
if (ok) {
std::cout << "解析成功: " << value << std::endl;
} else {
std::cout << "解析失败: " << msg << std::endl;
}
// 另一个例子:查找并返回索引和值
auto findInVector = [](const std::vector<int>& v, int target)
-> std::pair<bool, int> {
for (size_t i = 0; i < v.size(); ++i) {
if (v[i] == target) {
return {true, static_cast<int>(i)};
}
}
return {false, -1};
};
std::vector<int> data = {10, 20, 30, 40};
auto [found, index] = findInVector(data, 30);
if (found) {
std::cout << "找到元素,索引: " << index << std::endl;
}
return 0;
}
你可能会问:为什么不用std::optional?其实optional只能表示「有值」或「无值」,但如果你需要同时返回多个信息(比如状态码+错误消息),tuple或pair更合适。
避坑指南:我曾经在项目里用tuple返回了5个值,结果调用方根本记不清每个位置的语义。后来我改成定义一个小的结构体,代码可读性反而更好。所以我的建议是:超过3个值,考虑用结构体命名成员;3个及以下,tuple或pair完全够用。
知识体系图
下面这张图总结了pair、tuple和结构化绑定的核心关系:
从图中可以看到,pair是tuple的特例(二元组),而结构化绑定是C++17引入的通用解包机制。三者配合使用,基本覆盖了日常开发中所有「多值处理」的需求。
个人经验:我写工具函数时,经常用pair返回「是否成功+结果值」。调用方用结构化绑定接住,代码既安全又清晰。如果你还在用输出参数传引用,不妨试试这个模式——代码会干净很多。
公众号:蓝海资料掘金营,微信deep3321