C++ tuple

当我们需要把商品名称、购买数量和单价这 3 个甚至更多不同类型的数据临时打包成一个整体时,可以使用 C++ 标准库提供的 std::tuple(元组)。

前面学习的 std::pair 专门用来组合两个值,而 std::tuple 就相当于它的多元素扩展版,能够容纳任意固定数量的元素。

C++ tuple 的创建与访问

在 C++ 中,std::tuple 是定义在 <tuple> 头文件中的类模板。创建元组时,我们只需要在尖括号 <> 里按顺序写明各个位置的数据类型即可。

语法:

#include <tuple>

std::tuple<类型1, 类型2, ...> 变量名 = {值1, 值2, ...};

说明:

由于 tuple 可以包含任意多个元素,它不再像 pair 那样提供 first、second 这样的固定成员名,而是统一使用 std::get<N>(t) 来按位置读取或修改元组 t 中的元素。

尖括号里的位置索引 N 和数组下标一样从 0 开始计算:获取第 1 个元素写 std::get<0>(t),获取第 2 个元素写 std::get<1>(t),以此类推。另外需要注意,std::tuple 没有 .size() 成员函数,它的元素个数在声明类型时就已经固定下来了。

示例 1:创建与修改 tuple

#include <iostream>
#include <string>
#include <tuple>

int main() {
    std::tuple<std::string, int, double> t = {"Mouse", 2, 99.5};

    std::cout << std::get<0>(t) << '\n';    // 读取第 1 个元素
    std::cout << std::get<1>(t) << '\n';    // 读取第 2 个元素
    std::cout << std::get<2>(t) << '\n';    // 读取第 3 个元素

    std::get<1>(t) = 5;                     // 修改索引为 1 的值
    std::cout << std::get<1>(t) << '\n';
    return 0;
}

运行结果如下。

Mouse
2
99.5
5

分析:

上面代码中,元组 t 依次保存了 std::string、int 和 double 这 3 种类型的数据。我们通过 std::get<0>(t) 和 std::get<1>(t) 按位置读取里面的值,还可以通过 std::get<1>(t) = 5; 直接修改索引为 1 的元素。

与 pair 类似,<tuple> 头文件也提供了一个辅助函数 std::make_tuple(),用来根据传入的值自动推导类型。对于这个例子来说,下面 2 种创建方式是等价的:

// 方式 1(推荐)
std::tuple<std::string, int, double> t = {"Mouse", 2, 99.5};

// 方式 2
auto t = std::make_tuple(std::string("Mouse"), 2, 99.5);

在实际开发中,创建 tuple 变量时我们更推荐使用方式 1。直接在尖括号中写明每个位置的类型,不仅不用担心字符串字面量被推导成 const char*,阅读代码时也更加直观。

元素访问限制与大小比较

在使用 std::tuple 的过程中,我们需要搞清楚 std::get 的索引限制,以及两个元组之间的大小比较规则。

1. std::get 的索引限制

调用 std::get<N>(t) 访问元组元素时,小伙伴们需要遵守以下 2 条规则:

  • 索引不能越界:如果 tuple 只包含 3 个元素,合法的位置索引就只有 0、1、2。一旦填入了 3 或更大的数字,编译时就会直接报错。
  • 索引必须是编译期常量:尖括号 <> 里面属于模板参数,只能填写像 0、1 这样在编译时就已经确定的整数常量,不能把普通的变量放进去。

可能有些小伙伴会觉得奇怪:“为什么普通数组可以写 arr[i],而 std::get<i>(t) 却不行呢?” 这是因为数组里所有元素的类型都相同,不管 i 是几,取出来的都是同一种类型。而 tuple 每个位置上的类型可能完全不同,比如第 0 个位置是字符串,第 1 个位置是整数。

假如 i 是程序运行时才从键盘输入的数字,编译器在编译代码时就确定不了 std::get<i>(t) 到底应该返回字符串还是整数。因此,我们不能使用 for 循环配合变量 i 去遍历 tuple。对于只含 3 个元素的元组 t 来说,下面这两种写法都会在编译时报错:

int i = 0;
std::cout << std::get<3>(t) << '\n';    // 索引超出范围
std::cout << std::get<i>(t) << '\n';    // 不能传入普通变量

2. 比较两个 tuple 的大小

当两个 tuple 的元素个数相同,并且对应位置上的元素类型可以相互比较时,我们就可以直接使用 ==、!=、<、> 等运算符进行比较。它的比较规则与 std::pair 一致:从第 0 个位置开始往后逐项对比,一旦在某个位置分出大小就立即得出结果。

示例 2:比较 tuple 的大小

#include <iostream>
#include <tuple>

int main() {
    std::tuple<int, int, int> t1 = {1, 20, 300};
    std::tuple<int, int, int> t2 = {1, 25, 100};

    if (t1 < t2) {
        std::cout << "t1 小于 t2\n";
    }
    return 0;
}

运行结果如下。

t1 小于 t2

分析:

判断 t1 < t2 时,程序先对比第 0 个位置,发现两边都是 1。接着继续对比第 1 个位置,因为 t1 的 20 小于 t2 的 25,此时就已经分出大小并判定 t1 < t2 成立了,不会再往后比较第 2 个位置的 300 和 100。

使用 std::tie() 拆解 tuple

如果代码里到处都写 std::get<0>(t)、std::get<1>(t)、std::get<2>(t),不仅写起来繁琐,而且读代码时也很难一眼看出每个数字索引代表什么含义。

为了方便拆解数据,<tuple> 头文件提供了 std::tie() 函数。tie 在英文中是 “捆绑” 的意思,它可以把几个已经定义好的变量绑在一起,一次性接收 tuple 中的各个元素。

语法:

std::tie(变量1, 变量2, ...) = tuple变量;

说明:

调用 std::tie() 之前,小括号里的接收变量必须已经提前定义好。执行赋值后,程序会按顺序把 tuple 第 0 个位置的值赋给第 1 个变量,第 1 个位置的值赋给第 2 个变量,以此类推。

如果在拆解时不关心其中某一个位置的值,我们可以把 std::ignore 放在对应位置上占位。ignore 在英文中是 “忽略” 的意思,它就像一个专门用来占位的 “废纸篓”,会接住那个位置的值并直接丢弃掉。

示例 3:使用 tie() 拆解元组

#include <iostream>
#include <string>
#include <tuple>

int main() {
    std::tuple<std::string, int, double> t = {"Mouse", 2, 99.5};

    std::string name;
    double price = 0.0;

    // 拆解名称和单价
    std::tie(name, std::ignore, price) = t;

    std::cout << name << ": " << price << '\n';
    return 0;
}

运行结果如下。

Mouse: 99.5

分析:

std::tie(name, std::ignore, price) = t; 这句代码表示将元组 t 第 0 个位置的值赋给 name,将第 2 个位置的值赋给 price,而中间第 1 个位置的数量因为填了 std::ignore,会被直接跳过。

前面学习函数返回值时我们知道,一条 return 语句只能返回一个整体。如果希望函数一次带回 3 个结果,又应该怎么配合 std::tuple 和 std::tie() 来实现呢?我们可以看一下下面这个例子。

示例 4:函数返回多个结果

#include <iostream>
#include <string>
#include <tuple>

std::tuple<std::string, int, double> getStudentInfo() {
    return {"Lucy", 19, 96.5};    // 打包返回 3 项信息
}

int main() {
    std::string name;
    int age = 0;
    double score = 0.0;

    std::tie(name, age, score) = getStudentInfo();
    std::cout << name << ' ' << age << ' ' << score << '\n';
    return 0;
}

运行结果如下。

Lucy 19 96.5

分析:

在这个例子中,getStudentInfo() 函数将 "Lucy"、19 和 96.5 打包成一个 std::tuple 返回出来。随后在 main() 函数中,我们通过 std::tie(name, age, score) 一次性把返回值拆解到三个独立变量里,后面直接使用变量名即可。

提示 在 C++17 及更高版本中,如果不需要使用 std::ignore 跳过元素,大家还可以配合后面要学习的结构化绑定直接写成 auto [name, age, score] = getStudentInfo();,在一行代码里同时完成变量定义与数据拆解。

pair、tuple 与结构体怎么选?

std::pair、std::tuple 和结构体都能把多个不同类型的数据组合在一起。在实际开发中,我们可以按下面 3 种情况来选择:

  • std::pair:适合打包恰好两个成对出现的值,比如表示 “键 + 值”、“商 + 余数” 或 “起点 + 终点”,通过 .first 和 .second 就能直观访问。
  • std::tuple:适合在函数内部临时打包返回 3 个左右的结果,并且在调用处紧接着就会拆解到独立变量中使用,省去单独定义新类型的步骤。
  • struct 结构体:适合表示 “学生信息”、“商品订单” 这类含义明确的业务对象,或者需要在多个函数之间反复传递的数据。给每个成员起好 stu.name、stu.score 这样清晰的名字,后续维护代码会轻松很多。

上一篇:

下一篇:

给站长反馈

绿叶网正在不断完善中,小伙伴们如果发现任何问题,还望多多给站长反馈,谢谢!

邮箱:lvyenet@vip.qq.com

「绿叶网」服务号
绿叶网服务号放大
关注服务号,微信也能看教程。
绿叶网服务号