17.1. The tuple
Type
C++11A tuple is a template that is similar to a pair
(§ 11.2.3, p. 426). Each pair
type has different types for its members, but every pair
always has exactly two members. A tuple
also has members whose types vary from one tuple
type to another, but a tuple
can have any number of members. Each distinct tuple
type has a fixed number of members, but the number of members in one tuple
type can differ from the number of members in another.
A tuple
is most useful when we want to combine some data into a single object but do not want to bother to define a data structure to represent those data. Table 17.1 lists the operations that tuple
s support. The tuple
type, along with its companion types and functions, are defined in the tuple
header.
Table 17.1. Operations on tuple
s
Code | Description |
---|---|
tuple<T1, T2, ..., Tn> t; | t is a tuple with as many members as there are types T1 ...Tn . The members are value initialized (§ 3.3.1, p. 98). |
tuple<T1, T2, ..., Tn> t(v1, v2, ..., vn); | t is a tuple with types T1 ...Tn in which each member is initialized from the corresponding initializer vi . This constructor is explicit (§ 7.5.4, p. 296). |
make_tuple(v1, v2, ..., vn) | Returns a tuple initialized from the given initializers. The type of the tuple is inferred from the types of the initializers. |
t1 == t2 t1 != t2 | Two tuple s are equal if they have the same number of members and if each pair of members are equal. Uses each member's == operator. Once a member is found to be unequal, subsequent members are not tested. |
t1 relop t2 | Relational operations on tuple s using dictionary ordering (§ 9.2.7, p. 340). The tuples must have the same number of members. Members of t1 are compared with the corresponding members from t2 using the < operator. |
get<i>(t) | Returns a reference to the i th data member of t ; if it is an lvalue, the result is an lvalue reference; otherwise, it is an rvalue reference. All members of a tuple are public . |
tuple_size<tupleType>::value | A class template that can be instantiated by a tuple type and has a public constexpr static data member named size_t that is the number of members in the specified tuple type. |
tuple_element<i, tupleType>::type | A class template that can be instantiated by an integral constant and a tuple type and has a public member named type that is the type of the specified members in the specified tuple type. |
INFO
A tuple
can be thought of as a “quick and dirty” data structure.
17.1.1. Defining and Initializing tuple
s
When we define a tuple
, we name the type(s) of each of its members:
tuple<size_t, size_t, size_t> threeD; // all three members set to 0
tuple<string, vector<double>, int, list<int>>
someVal("constants", {3.14, 2.718}, 42, {0,1,2,3,4,5});
When we create a tuple
object, we can use the default tuple
constructor, which value initializes (§ 3.3.1, p. 98) each member, or we can supply an initializer for each member as we do in the initialization of someVal
. This tuple
constructor is explicit
(§ 7.5.4, p. 296), so we must use the direct initialization syntax:
tuple<size_t, size_t, size_t> threeD = {1,2,3}; // error
tuple<size_t, size_t, size_t> threeD{1,2,3}; // ok
Alternatively, similar to the make_pair
function (§ 11.2.3, p. 428), the library defines a make_tuple
function that generates a tuple
object:
// tuple that represents a bookstore transaction: ISBN, count, price per book
auto item = make_tuple("0-999-78345-X", 3, 20.00);
Like make_pair
, the make_tuple
function uses the types of the supplied initializers to infer the type of the tuple
. In this case, item
is a tuple
whose type is tuple<const char*, int
, double>
.
Accessing the Members of a tuple
A pair
always has two members, which makes it possible for the library to give these members names (i.e., first
and second
). No such naming convention is possible for tuple
because there is no limit on the number of members a tuple
type can have. As a result, the members are unnamed. Instead, we access the members of a tuple
through a library function template named get
. To use get
we must specify an explicit template argument (§ 16.2.2, p. 682), which is the position of the member we want to access. We pass a tuple
object to get
, which returns a reference to the specified member:
auto book = get<0>(item); // returns the first member of item
auto cnt = get<1>(item); // returns the second member of item
auto price = get<2>(item)/cnt; // returns the last member of item
get<2>(item) *= 0.8; // apply 20% discount
The value inside the brackets must be an integral constant expression (§ 2.4.4, p. 65). As usual, we count from 0, meaning that get<0>
is the first member.
If we have a tuple
whose precise type details we don’t know, we can use two auxilliary class templates to find the number and types of the tuple
’s members:
typedef decltype(item) trans; // trans is the type of item
// returns the number of members in object's of type trans
size_t sz = tuple_size<trans>::value; // returns 3
// cnt has the same type as the second member in item
tuple_element<1, trans>::type cnt = get<1>(item); // cnt is an int
To use tuple_size
or tuple_element
, we need to know the type of a tuple
object. As usual, the easiest way to determine an object’s type is to use decltype
(§ 2.5.3, p. 70). Here, we use decltype
to define a type alias for the type of item
, which we use to instantiate both templates.
tuple_size
has a public static
data member named value
that is the number or members in the specified tuple
. The tuple_element
template takes an index as well as a tuple type. tuple_element
has a public
type member named type
that is the type of the specified member of the specified tuple
type. Like get, tuple_element
uses indices starting at 0.
Relational and Equality Operators
The tuple
relational and equality operators behave similarly to the corresponding operations on containers (§ 9.2.7, p. 340). These operators execute pairwise on the members of the left-hand and right-hand tuple
s. We can compare two tuple
s only if they have the same number of members. Moreover, to use the equality or inequality operators, it must be legal to compare each pair of members using the ==
operator; to use the relational operators, it must be legal to use <
. For example:
tuple<string, string> duo("1", "2");
tuple<size_t, size_t> twoD(1, 2);
bool b = (duo == twoD); // error: can't compare a size_t and a string
tuple<size_t, size_t, size_t> threeD(1, 2, 3);
b = (twoD < threeD); // error: differing number of members
tuple<size_t, size_t> origin(0, 0);
b = (origin < twoD); // ok: b is true
INFO
Because tuple
defines the <
and ==
operators, we can pass sequences of tuple
s to the algorithms and can use a tuple
as key type in an ordered container.
INFO
Exercises Section 17.1.1
Exercise 17.1: Define a tuple
that holds three int
values and initialize the members to 10, 20
, and 30
.
Exercise 17.2: Define a tuple
that holds a string
, a vector<string>
, and a pair<string, int>
.
Exercise 17.3: Rewrite the TextQuery
programs from § 12.3 (p. 484) to use a tuple
instead of the QueryResult
class. Explain which design you think is better and why.
17.1.2. Using a tuple
to Return Multiple Values
A common use of tuple
is to return multiple values from a function. For example, our bookstore might be one of several stores in a chain. Each store would have a transaction file that holds data on each book that the store recently sold. We might want to look at the sales for a given book in all the stores.
We’ll assume that we have a file of transactions for each store. Each of these per-store transaction files will contain all the transactions for each book grouped together. We’ll further assume that some other function reads these transaction files, builds a vector<Sales_data>
for each store, and puts those vector
s in a vector
of vector
s:
// each element in files holds the transactions for a particular store
vector<vector<Sales_data>> files;
We’ll write a function that will search files
looking for the stores that sold a given book. For each store that has a matching transaction, we’ll create a tuple
to hold the index of that store and two iterators. The index will be the position of the matching store in files
. The iterators will mark the first and one past the last record for the given book in that store’s vector<Sales_data>
.
A Function That Returns a tuple
We’ll start by writing the function to find a given book. This function’s arguments are the vector
of vector
s just described, and a string
that represents the book’s ISBN. Our function will return a vector
of tuple
s that will have an entry for each store with at least one sale for the given book:
// matches has three members: an index of a store and iterators into that store's vector
typedef tuple<vector<Sales_data>::size_type,
vector<Sales_data>::const_iterator,
vector<Sales_data>::const_iterator> matches;
// files holds the transactions for every store
// findBook returns a vector with an entry for each store that sold the given book
vector<matches>
findBook(const vector<vector<Sales_data>> &files,
const string &book)
{
vector<matches> ret; // initially empty
// for each store find the range of matching books, if any
for (auto it = files.cbegin(); it != files.cend(); ++it) {
// find the range of Sales_data that have the same ISBN
auto found = equal_range(it->cbegin(), it->cend(),
book, compareIsbn);
if (found.first != found.second) // this store had sales
// remember the index of this store and the matching range
ret.push_back(make_tuple(it - files.cbegin(),
found.first, found.second));
}
return ret; // empty if no matches found
}
The for
loop iterates through the elements in files
. Those elements are themselves vector
s. Inside the for
we call a library algorithm named equal_range
, which operates like the associative container member of the same name (§ 11.3.5, p. 439). The first two arguments to equal_range
are iterators denoting an input sequence (§ 10.1, p. 376). The third argument is a value. By default, equal_range
uses the <
operator to compare elements. Because Sales_data
does not have a <
operator, we pass a pointer to the compareIsbn
function (§ 11.2.2, p. 425).
The equal_range
algorithm returns a pair
of iterators that denote a range of elements. If book
is not found, then the iterators will be equal, indicating that the range is empty. Otherwise, the first
member of the returned pair
will denote the first matching transaction and second
will be one past the last.
Using a tuple
Returned by a Function
Once we have built our vector
of stores with matching transactions, we need to process these transactions. In this program, we’ll report the total sales results for each store that has a matching sale:
void reportResults(istream &in, ostream &os,
const vector<vector<Sales_data>> &files)
{
string s; // book to look for
while (in >> s) {
auto trans = findBook(files, s); // stores that sold this book
if (trans.empty()) {
cout << s << " not found in any stores" << endl;
continue; // get the next book to look for
}
for (const auto &store : trans) // for every store with a sale
// get<n> returns the specified member from the tuple in store
os << "store " << get<0>(store) << " sales: "
<< accumulate(get<1>(store), get<2>(store),
Sales_data(s))
<< endl;
}
}
The while
loop repeatedly reads the istream
named in
to get the next book to process. We call findBook
to see if s
is present, and assign the results to trans
. We use auto
to simplify writing the type of trans
, which is a vector
of tuple
s.
If trans
is empty, there were no sales for s
. In this case, we print a message and return to the while
to get the next book to look for.
The for
loop binds store
to each element in trans
. Because we don’t intend to change the elements in trans
, we declare store
as a reference to const
. We use get
to print the relevant data: get<0>
is the index of the corresponding store, get<1>
is the iterator denoting the first transaction, and get<2>
is the iterator one past the last.
Because Sales_data
defines the addition operator (§ 14.3, p. 560), we can use the library accumulate
algorithm (§ 10.2.1, p. 379) to sum the transactions. We pass a Sales_data
object initialized by the Sales_data
constructor that takes a string
(§ 7.1.4, p. 264) as the starting point for the summation. That constructor initializes the bookNo
member from the given string
and the units_sold
and revenue
members to zero.
INFO
Exercises Section 17.1.2
Exercise 17.4: Write and test your own version of the findBook
function.
Exercise 17.5: Rewrite findBook
to return a pair
that holds an index and a pair
of iterators.
Exercise 17.6: Rewrite findBook
so that it does not use tuple
or pair
.
Exercise 17.7: Explain which version of findBook
you prefer and why.
Exercise 17.8: What would happen if we passed Sales_data()
as the third parameter to accumulate
in the last code example in this section?