【C++基础】第九十八课:[标准库特殊设施]tuple类型

tuple

Posted by x-jeff on May 20, 2024

【C++基础】系列博客为参考《C++ Primer中文版(第5版)》C++11标准)一书,自己所做的读书笔记。
本文为原创文章,未经本人允许,禁止转载。转载请注明出处。

1.tuple类型

tuple是类似pair的模板。每个pair的成员类型都不相同,但每个pair都恰好有两个成员。不同tuple类型的成员类型也不相同,但一个tuple可以有任意数量的成员。每个确定的tuple类型的成员数目是固定的,但一个tuple类型的成员数目可以与另一个tuple类型不同。

当我们希望将一些数据组合成单一对象,但又不想麻烦地定义一个新数据结构来表示这些数据时,tuple是非常有用的。表17.1列出了tuple支持的操作。tuple类型及其伴随类型和函数都定义在tuple头文件中。

我们可以将tuple看作一个“快速而随意”的数据结构。

2.定义和初始化tuple

当我们定义一个tuple时,需要指出每个成员的类型:

1
2
tuple<size_t, size_t, size_t> threeD; //三个成员都设置为0
tuple<string, vector<double>, int, list<int>> someVal("constants", {3.14, 2.718}, 42, {0,1,2,3,4,5})

tuple的这个构造函数是explicit的,因此我们必须使用直接初始化语法:

1
2
tuple<size_t, size_t, size_t> threeD = {1,2,3}; //错误
tuple<size_t, size_t, size_t> threeD{1,2,3}; //正确

类似make_pair函数,标准库定义了make_tuple函数,我们还可以用它来生成tuple对象:

1
2
//表示书店交易记录的tuple,包含:ISBN、数量和每册书的价格
auto item = make_tuple("0-999-78345-X", 3, 20.00);

类似make_pair,make_tuple函数使用初始值的类型来推断tuple的类型。在本例中,item是一个tuple,类型为tuple<const char*, int, double>

2.1.访问tuple的成员

一个pair总是有两个成员,这样,标准库就可以为它们命名(如,first和second)。但这种命名方式对tuple是不可能的,因为一个tuple类型的成员数目是没有限制的。因此,tuple的成员都是未命名的。要访问一个tuple的成员,就要使用一个名为get的标准库函数模板。为了使用get,我们必须指定一个显式模板实参,它指出我们想要访问第几个成员。我们传递给get一个tuple对象,它返回指定成员的引用:

1
2
3
4
auto book = get<0>(item); //返回item的第一个成员
auto cnt = get<1>(item); //返回item的第二个成员
auto price = get<2>(item)/cnt; //返回item的最后一个成员
get<2>(item) *= 0.8; //打折20%

尖括号中的值必须是一个整型常量表达式

如果不知道一个tuple准确的类型细节信息,可以用两个辅助类模板来查询tuple成员的数量和类型:

1
2
3
4
5
typedef decltype(item) trans; //trans是item的类型
//返回trans类型对象中成员的数量
size_t sz = tuple_size<trans>::value; //返回3
//cnt的类型与item中第二个成员相同
tuple_element<1, trans>::type cnt = get<1>(item); //cnt是一个int

2.2.关系和相等运算符

tuple的关系和相等运算符的行为类似容器的对应操作。这些运算符逐对比较左侧tuple和右侧tuple的成员。只有两个tuple具有相同数量的成员时,我们才可以比较它们。而且,为了使用tuple的相等或不等运算符,对每对成员使用==运算符必须都是合法的;为了使用关系运算符,对每对成员使用<必须都是合法的。例如:

1
2
3
4
5
6
7
tuple<string, string> duo("1", "2");
tuple<size_t, size_t> twoD(1, 2);
bool b = (duo == twoD); //错误:不能比较size_t和string
tuple<size_t, size_t, size_t> threeD(1, 2, 3);
b = (twoD < threeD); //错误:成员数量不同
tuple<size_t, size_t> origin(0, 0);
b = (origin < twoD); //正确:b为true

由于tuple定义了<==运算符,我们可以将tuple序列传递给算法,并且可以在无序容器中将tuple作为关键字类型。

3.使用tuple返回多个值

tuple的一个常见用途是从一个函数返回多个值。例如,我们的书店可能是多家连锁书店中的一家。每家书店都有一个销售记录文件,保存每本书近期的销售数据。我们可能希望在所有书店中查询某本书的销售情况。

假定每家书店都有一个销售记录文件。每个文件都将每本书的所有销售记录存放在一起。进一步假定已有一个函数可以读取这些销售记录文件,为每个书店创建一个vector<Sales_data>,并将这些vector保存在vector的vector中:

1
2
//files中的每个元素保存一家书店的销售记录
vector<vector<Sales_data>> files;

我们将编写一个函数,对于一本给定的书,在files中搜索出售过这本书的书店。对每家有匹配销售记录的书店,我们将创建一个tuple来保存这家书店的索引和两个迭代器。索引指出了书店在files中的位置,而两个迭代器则标记了给定书籍在此书店的vector<Sales_data>中第一条销售记录和最后一条销售记录之后的位置。

3.1.返回tuple的函数

我们首先编写查找给定书籍的函数。此函数的参数是刚刚提到的vector的vector以及一个表示书籍ISBN的string。我们的函数将返回一个tuple的vector,凡是销售了给定书籍的书店,都在vector中有对应的一项:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//matches有三个成员:一家书店的索引和两个指向书店vector中元素的迭代器
typedef tuple<vector<Sales_data>::size_type, vector<Sales_data>::const_iterator, vector<Sales_data>::const_iterator> matches;
//files保存每家书店的销售记录
//findBook返回一个vector,每家销售了给定书籍的书店在其中都有一项
vector<matches>
findBook(const vector<vector<Sales_data>> &files, const string &book)
{
	vector<matches> ret; //初始化为空vector
	//对每家书店,查找与给定书籍匹配的记录范围(如果存在的话)
	for (auto it = files.cbegin(); it != files.cend(); ++it) {
		//查找具有相同ISBN的Sales_data范围
		auto found = equal_range(it->cbegin(), it->end(), book, compareIsbn);
		if (found.first != found.second) //此书店销售了给定书籍
			//记住此书店的索引及匹配的范围
			ret.push_back(make_tuple(it - files.cbegin(), found.first, found.second));
	}
	return ret; //如果未找到匹配记录的话,ret为空
}

在for循环内,我们调用了一个名为equal_range的标准库算法,它的功能与关联容器的同名成员类似。equal_range的前两个实参是表示输入序列的迭代器(参见:概述),第三个参数是一个值。默认情况下,equal_range使用<运算符来比较元素。由于Sales_data没有<运算符,因此我们传递给它一个指向compareIsbn函数的指针。

equal_range算法返回一个迭代器pair,表示元素的范围。如果未找到book,则两个迭代器相等,表示空范围。否则,返回的pair的first成员将表示第一条匹配的记录,second则表示匹配的尾后位置。

3.2.使用函数返回的tuple

一旦我们创建了vector保存包含匹配的销售记录的书店,就需要处理这些记录了。在此程序中,对每家包含匹配销售记录的书店,我们将打印其汇总销售信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void reportResults(istream &in, ostream &os, const vector<vector<Sales_data>> &files)
{
	string s; //要查找的书
	while (in >> s) {
		auto trans = findBook(files, s); //销售了这本书的书店
		if (trans.empty()) {
			cout << s << " not found in any stores" << endl;
			continue; //获得下一本要查找的书
		}
		for (const auto &store : trans) //对每家销售了给定书籍的书店
			//get<n>返回store中tuple的指定的成员
			os << "store " << get<0>(store) << " sales: " << accumulate(get<1>(store), get<2>(store), Sales_data(s)) << endl;
	}
}

由于Sales_data定义了加法运算符,因此我们可以用标准库的accumulate算法来累加销售记录。我们用Sales_data的接受一个string参数的构造函数来初始化一个Sales_data对象,将此对象传递给accumulate作为求和的起点。此构造函数用给定的string初始化bookNo,并将units_sold和revenue成员置为0。