HOWTO · C++

如何在 C++ 中创建、访问和打印 `std::tuple` 值

学习如何在 C++ 中构造异构 `std::tuple` 值、读取和更新其元素,并打印不同大小的元组。

本页内容

当需要把名称、标识符和分数等类型不同、数量固定的字段组合成一个值时,可以使用 std::tuple。本指南将介绍如何使用 std::make_tuple 构造这类值,使用 std::get 读取和更新其中的元素,以及如何打印不同大小元组中的所有元素。元组元素的位置属于其类型的一部分,因此适合表示字段已知的小型记录,而不适合表示运行时会增长的集合。

std::tuple 存储什么,以及何时使用它

std::tuple 是一种类模板,用于表示大小固定的值序列。每个位置都可以具有不同类型,而且所有位置的类型和顺序在编译时就已确定。例如,元组可以同时保存字符串、整数和浮点数。std::tuple 是一种类型,不是函数或命令。

元组的位置没有名称,因此适合表示简短的结果或局部数据组合;但如果程序中需要始终使用含义明确的字段名,元组的表达力就较弱。若命名字段能让数据更易理解,可以使用结构体;若元素数量会变化,则应使用容器。

使用 std::make_tuple 构造元组

std::make_tuple 接收多个值并推导出相应的元素类型,因此构造时无需写出冗长的 std::tuple<...> 类型。元组会按照实参顺序保存这些值。使用 std::tuple 和 std::make_tuple 时要包含 <tuple>;存储 std::string 值时还要包含 <string>。在下面的完整程序中,shortTuple 由一个 std::string、一个 int 和一个 double 构造而成;std::make_tuple 会根据实参推导出这些元素类型。值会根据各自的类型被复制或移动到元组中。这是按值构造,并不能创建大小可动态变化的序列。

使用 std::get 读取和更新元组元素

使用 std::get<I>(value) 访问位置 I 上的元素,其中 I 是从零开始的编译时索引。对于有三个元素的元组,有效位置是 0、1 和 2。在下面的完整程序中,std::get<1>(profile) 会将整数元素读入 originalId,同一个表达式也可以放在赋值语句左侧来更新该元素。所选位置必须有效,因此不能把只在运行时计算出的索引传给 std::get。

std::tuple 不支持方括号索引。如果索引需要在运行时选择,可以改用其他表示方式,例如元素类型相同的容器;也可以针对元组已知的位置编写显式分派。通过 std::get 读取元素时会保留其类型,因此编译器可以根据实际字段类型检查赋值和函数调用。

使用可变参数模板打印每个元素

可变参数模板可以接受元素数量不同的元组。下面的示例要求使用 C++17 或更高版本,因为其中使用了折叠表达式。它通过 std::index_sequence 生成编译时位置,再用折叠表达式将每个对应的值写入输出流。输出会用括号包围各个值,并以逗号分隔;每种元素类型都必须支持插入 std::ostream。程序会构造并更新一个元组,然后打印另外两个大小不同的元组。输出如下:

运行此示例:

本节中的代码以及显示的标签或输出仍为英文。

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

using namespace std;

template <class Tuple, size_t... I>
void printTupleImpl(const Tuple& values, index_sequence<I...>) {
  size_t printed = 0;
  ((cout << (printed++ ? ", " : "") << get<I>(values)), ...);
}

template <class... Values>
void printTuple(const tuple<Values...>& values) {
  cout << "(";
  printTupleImpl(values, index_sequence_for<Values...>{});
  cout << ")\n";
}

int main() {
  auto profile = make_tuple(string("Ada"), 7, 9.5);
  const int originalId = get<1>(profile);
  get<1>(profile) = originalId + 1;

  auto shortTuple = make_tuple(string("Ada"), 7, 9.5);
  auto longTuple = make_tuple(string("Lin"), 12, 8.25, string("C++"));
  printTuple(shortTuple);
  printTuple(longTuple);
}
(Ada, 7, 9.5)
(Lin, 12, 8.25, C++)

辅助函数会针对 std::index_sequence_for 提供的每个索引展开一个 std::get<I> 表达式。逗号折叠会按顺序处理这些表达式,因此同一个函数可以处理两种元组长度,无需为每种长度分别编写重载。它也能处理空元组:折叠表达式不会执行任何插入操作,函数仍会输出两侧的括号。待打印的值仍须支持流插入;若元组含有自定义类型,就需要为该类型提供合适的流插入运算符重载。

如何选择 std::tuple 或 std::pair

std::pair 恰好存储两个值,而 std::tuple 可以存储任意固定数量的值,包括两个。如果数据天然由两个相关字段组成,例如键和值,优先考虑 std::pair,因为它的类型直接表达了这种结构。如果要组合三个或更多类型不同的值,或者某个接口有意一次返回多个值,则可以选择 std::tuple。

对于会在代码库多处使用的数据,命名结构体通常比这两种类型都更能表达意图。对于临时组合或简洁的返回值,std::tuple 可以省去定义单独类型的步骤。无论选择哪种方式,都应明确字段顺序,并使用 std::get 访问元组位置,让读者知道每个索引对应哪个值。