RIP index_sequence, 2014–2017

Matt Aubury
3 min readMar 2, 2018

--

It was good while it lasted…

std::tuple was one of the great additions to C++11. Whilst sometimes abused by lazy programmers (who should really be using a struct or class), it’s true value is as a container of arbitrary values in variadic templates.

Sadly, static typing makes working with tuples much harder in C++ than in most languages. A simple operation like printing out each member requires a slightly crazy three function dance in C++11:

template<size_t I, typename... Ts>
void print_tuple_impl (const std::tuple<Ts...> &tuple)
{
std::cout << std::get<I> (tuple) << '\n';
if (I + 1 < sizeof... (Ts))
{
print_tuple_impl<(I + 1 < sizeof... (Ts) ? I + 1 : I)>
(tuple);
}
}
template<typename... Ts>
void print_tuple (const std::tuple<Ts...> &tuple)
{
print_tuple_impl<0> (tuple);
}
template<>
void print_tuple<> (const std::tuple<> &)
{
}

There’s more than one way to skin this particular cat, but none of them are much simpler.

Matters improved in C++14 with the introduction of std::index_sequence, which gave us a way of iterating tuples:

template<typename... Ts, size_t... I>
void print_tuple_impl (const std::tuple<Ts...> &tuple,
std::index_sequence<I...>)
{
(void)(std::initializer_list<int>
{ (std::cout << std::get<I> (tuple) << '\n', 0)... });
}
template<typename… Ts>
void print_tuple (const std::tuple<Ts...> &tuple)
{
print_tuple_impl (tuple, std::index_sequence_for<Ts...> {});
}

We’re down to just two functions, but there is still far more magic here than is desirable.

C++17 has added two new features which allow us to write something far better. The first is std::apply, a library feature for unpacking tuples back into parameter packs. The second is fold expressions, a language feature which extends the ways in which those parameter packs can be combined.

Putting these two together our function becomes:

template<typename... Ts>
void print_tuple (const std::tuple<Ts...> &tuple)
{
std::apply ([] (const auto &... item)
{
((std::cout << item << '\n'), ...);
},
tuple);
}

Much better, at least in my opinion!

The lambda that we pass to std::apply can also return values. Combining this with other fold operators, we can write a function that (say) adds the sizes of each member of a tuple:

template<typename... Ts>
size_t total_size (const std::tuple<Ts...> &tuple)
{
return std::apply ([] (const auto &... item)
{
return (std::size (item) + ...);
},
tuple);
}

Sometimes we need to work on pairs of tuples, matching up each element in turn. We can’t do this with a single call to std::apply, instead we need to expand each tuple inside nested calls. For example, to zip a pair-of-tuples into a tuple-of-pairs:

template<typename... Ts, typename... Us>
auto zip_tuples (const std::tuple<Ts...> &tuple_t,
const std::tuple<Us...> &tuple_u)
{
return std::apply (
[&] (const auto &... ts)
{
return std::apply (
[&] (const auto &... us)
{
return std::tuple (std::pair (ts, us)...);
},
tuple_u);
},
tuple_t);
}

Finally, there are times when we want to treat one element of the tuple differently to the others. For example, if we wanted to print the tuple with comma separators we could pick the first item off in the lambda parameter list:

template<typename... Ts>
void print_tuple (std::tuple<Ts...> tuple)
{
if constexpr (sizeof... (Ts) > 0)
{
std::apply ([] (const auto &item, const auto &... items)
{
std::cout << item;
((std::cout << ", " << items), ...);
},
tuple);
}
}

Note that we need aconstexpr guard here to avoid a compile-time error with empty tuples.

I’m calling this the apply-reduce idiom (unless anyone has a better idea), and I think it makes std::index_sequence redundant much of the time. Let me know what you think!

--

--