Team LiB
Previous Section Next Section

17.1. The tuple Type

 
Image

A tuple is a template that is similar to a pair11.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 tuples support. The tuple type, along with its companion types and functions, are defined in the tuple header.

 

Table 17.1. Operations on tuples

 
Image
 

Image Note

A tuple can be thought of as a “quick and dirty” data structure.

 

 

17.1.1. Defining and Initializing tuples

 

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 explicit7.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 decltype2.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 tuples. We can compare two tuples 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

 

Image Note

Because tuple defines the < and == operators, we can pass sequences of tuples to the algorithms and can use a tuple as key type in an ordered container.

 

 

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 vectors in a vector of vectors:

 

 

// 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 vectors just described, and a string that represents the book’s ISBN. Our function will return a vector of tuples 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 vectors. 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 tuples.

 

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 string7.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 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?


 
Team LiB
Previous Section Next Section