2.5.8 Tuple

What was the problem

The type std::pair is a nice utility class available in the STL since forever. It is a struct accepting two template parameters and defining two fields of these types, named “first” and “second”. Approximately like:

template<typename T1, typename T2> 
struct pair 
{ 
  T1 first; 
  T2 second; 
};

It is for example the value type of associative containers like std::map, and now we are stuck with map entries with fields named “first” and “second” while something like “key” and “value” would have carried the semantics better. Meh. Well, it may not be the most expressive but at least we are reusing code via this hyper generic reusable type, hooray!

Sorry, I inadvertently switched the rant-mode button on9.

How the Problem is Solved

Generalizing this structure to any number of fields/types was quite complex, as before the arrival of variadic templates [2.3.3] in C++11 we had to go through some type lists and other metaprogramming dances.

PIC <tuple>

Thanks to their introduction, generalizing std::pair to any number of elements is now somewhat simpler, and it is already done in the STL via std::tuple. Additionally, std::make_tuple() is a utility function that can create a tuple from its values, deducing the actual tuple type from its arguments. Finally, the main companion function to std::tuple is std::get(), which allows to access a tuple element by its index.

// Creating a tuple explicitly. 
std::tuple<int, float, bool> t(24, 42.f, true); 
 
// Creating a tuple from its values. 
t = std::make_tuple(42, 24.f, false); 
 
// Accessing the elements in a tuple. 
printf("%f\n", std::get<1>(t)); 
std::get<2>(t) = false;

Additionally, std::tie() is a helper function which creates a tuple whose elements are references to the arguments passed to the function.

// Swap the first mismatching elements from two vectors. 
void swap_first_mismatch(std::vector<int>& v1, std::vector<int>& v2) 
{ 
  using iterator = std::vector<int>::iterator; 
 
  iterator it_v1; 
  iterator it_v2; 
 
  // std::mismatch() returns a pair of iterators. We assign this pair 
  // to a tuple whose elements are references to the two variables 
  // declared above, meaning that we actually assign the variables. 
  std::tie(it_v1, it_v2) = std::mismatch(v1.begin(), v1.end(), v2.begin()); 
 
  std::swap(*it_v1, *it_v2); 
}

Declaring a tuple in a C++ program typically happen for few reasons:

Guideline

If you ever end up in a situation where std::tuple seems to be the best type for an aggregate value, please reconsider.

Naming things is hard, but having a named struct will be better to carry the meaning to the next readers than presenting them a bunch of data thrown in a bag.

It is thus quite difficult to find a good short example for std::tuple. One good example could be argument binding, but it’s a quite long example that needs many extra features. Another example is found in one constructor of std::pair, which allows to directly pass the arguments to use to construct its fields:

template<typename T, typename U> 
struct pair 
{ 
  // Simplified for the example. 
  template<typename... FirstArgs, typename... SecondArgs> 
  pair 
  (std::tuple<FirstArgs...> first_args, std::tuple<SecondArgs...> second_args) 
    : first(/* here we pass the content of first_args */), 
      second(/* here we pass the content of second_args */) 
  {}

Yet another example is a metaprogramming use case where multiple parameter packs must be passed to a type or function:

// This won’t work since the compiler cannot tell where to split the 
// As and Bs 
template<typename... As, typename... Bs> 
struct failing_multi_pack 
{}; 
 
// The code below will work though. 
 
// This one is just the base template declaration, not defined. 
template<typename As, typename Bs> 
struct working_multi_pack; 
 
// And we can specialize it for tuples. Now the compiler can split As 
// and Bs. 
template<typename... As, typename... Bs> 
struct working_multi_pack<std::tuple<As>, std::tuple<Bs>> 
{};

Well, I can see that you are a bit disappointed. Let’s have a look at the binding stuff. Hold on, it is not simple (still incomplete however):

#include <tuple> 
#include <cstdio> 
 
// This structure is here to "store" a sequence of integers in its template 
// parameters. There is nothing like that in C++11 but it is already available 
// in C++14 3.2.2. 
template<unsigned... I> 
struct integer_sequence {}; 
 
// This one is used to create a sequence of N consecutive integers, from 0 to 
// N-1, given N. 
// 
// Remaining is the number of integers that still have to be generated. 
// Computed is the sequence we have computed so far. 
template<unsigned Remaining, unsigned... Computed> 
struct make_integer_sequence; 
 
template<unsigned... Computed> 
struct make_integer_sequence<0, Computed...> 
{ 
  using type = integer_sequence<Computed...>; 
}; 
 
template<unsigned Remaining, unsigned... Computed> 
struct make_integer_sequence 
{ 
  using type = 
    typename make_integer_sequence 
    < 
      Remaining - 1, 
      Remaining - 1, 
      Computed... 
    >::type; 
}; 
 
// The call_helper will help us to get the elements of a tuple, because we 
// cannot get them by type. 
template<typename IntegerSequence> 
struct call_helper; 
 
template<unsigned... I> 
struct call_helper<integer_sequence<I...>> 
{ 
  template<typename F, typename... Args> 
  static void call(F&& function, std::tuple<Args...>& arguments) 
  { 
    // Here we unpack I... to get the arguments from the tuple. 
    // See [2.3.3]. 
    // 
    // Note that it does not work with member functions, as they require another 
    // call syntax. This is left as an exercise for the reader. 
    function(std::get<I>(arguments)...); 
  } 
}; 
 
// A binding is a function object that can be called with no arguments and that, 
// when called, will pass the arguments given to its constructor to the function 
// given to its constructor. 
template<typename F, typename... Args> 
struct binding 
{ 
  binding(F&& f, Args&&... arguments) 
    : m_function(std::forward<F>(f)), 
      m_arguments(std::forward<Args>(arguments)...) 
  {} 
 
  void operator()() 
  { 
    // Here there is no way to get the elements from the m_arguments tuple by 
    // unpacking Args..., so we indirectly build an integer pack that will 
    // ultimately be used to call std::get<I>(m_arguments) for each I. 
    call_helper 
      < 
        typename make_integer_sequence<sizeof...(Args)>::type 
      >::call 
      (m_function, m_arguments); 
  } 
 
private: 
  F m_function; 
  std::tuple<Args...> m_arguments; 
}; 
 
// This function is just here to build a binding without specifying all its 
// template parameters. 
template<typename F, typename... Args> 
binding<F, Args...> bind(F&& f, Args&&... arguments) 
{ 
  return 
    binding<F, Args...> 
    (std::forward<F>(f), std::forward<Args>(arguments)...); 
} 
 
// The function that will be bound. 
void print(int a, int b) 
{ 
  printf("%d, %d\n", a, b); 
} 
 
int main() 
{ 
  // This creates the binding. Is it a valid use case of auto [2.1.9]? 
  auto f(bind(&print, 24, 42)); 
 
  // And now we can call print with the provided arguments. 
  f(); 
 
  return 0; 
}

9That being said, I actually love the fact that even for small types like that effort is made by C++ developers to build more efficient implementations. Search for compressed pair or tight pair for example.

10Don’t do that.

11Don’t do that either, just write a meaningful type.

12But… why should we lure them if it’s not their kind of stuff?