C++ Tuples: the missing functionality

C++ provides a strange mix of compile-time and runtime functionality for dealing with tuples. There are some interesting parts, like std::tie to destructure a tuple, and std::tuple_cat to join together several tuples into one. So there is evidence that the standard has been influenced by some functional programming ideas, but I don’t think the full power of tuples has been realized (in both senses), and I found myself thinking about some missing parts.

Like why do we have std::tuple_cat and std::get<0> (aka std::tuple_head), but not std::tuple_cons or std::tuple_tail? So here are some possible implementations.

First, something missing from type_traits which will help us constrain things:

template <typename T>
struct is_tuple : public std::false_type {};
 
template <typename... Ts>
struct is_tuple<std::tuple<Ts...>> : public std::true_type {};

Both tuple_cons and tuple_tail can make use of an expansion of std::index_sequence to work their magic, with a user-facing function that provides the appropriate std::index_sequence to an overload.

For tuple_cons, we just call std::make_tuple with the new element and the result of expanding std::get across the other tuple:

template <typename U, typename T, std::size_t ...Is>
auto tuple_cons(U&& u, T&& t, std::index_sequence<Is...>,
    std::enable_if_t<is_tuple<std::decay_t<T>>::value>* = nullptr)
{
  return std::make_tuple(std::forward<U>(u), 
                         std::get<Is>(std::forward<T>(t))...);
}
 
template <typename U, typename T>
auto tuple_cons(U&& u, T&& t,
    std::enable_if_t<is_tuple<std::decay_t<T>>::value>* = nullptr)
{
  using Tuple = std::decay_t<T>;
  return tuple_cons(std::forward<U>(u), std::forward<T>(t),
      std::make_index_sequence<std::tuple_size<Tuple>::value>());
}

And for tuple_tail, we construct a std::index_sequence of length n-1, then offset it by one in the expansion to obtain the tail:

template <typename T, std::size_t ...Is>
auto tuple_tail(T&& t, std::index_sequence<Is...>,
    std::enable_if_t<is_tuple<std::decay_t<T>>::value>* = nullptr)
{
  return std::make_tuple(std::get<Is + 1>(std::forward<T>(t))...);
}
 
template <typename T>
auto tuple_tail(T&& t, 
    std::enable_if_t<is_tuple<std::decay_t<T>>::value>* = nullptr)
{
  using Tuple = std::decay_t<T>;
  return tuple_tail(std::forward<T>(t),
      std::make_index_sequence<std::tuple_size<Tuple>::value - 1>());
}

Leave a Reply