tuple
TypeA 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
tuple
sWhen 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>
.
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.
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
Because
tuple
defines the<
and==
operators, we can pass sequences oftuple
s to the algorithms and can use atuple
as key type in an ordered container.
Exercises Section 17.1.1
Exercise 17.1: Define a
tuple
that holds threeint
values and initialize the members to10, 20
, and30
.Exercise 17.2: Define a
tuple
that holds astring
, avector<string>
, and apair<string, int>
.Exercise 17.3: Rewrite the
TextQuery
programs from § 12.3 (p. 484) to use atuple
instead of theQueryResult
class. Explain which design you think is better and why.
tuple
to Return Multiple ValuesA 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>
.
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.
tuple
Returned by a FunctionOnce 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.
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 apair
that holds an index and apair
of iterators.Exercise 17.6: Rewrite
findBook
so that it does not usetuple
orpair
.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 toaccumulate
in the last code example in this section?