Purpose of std::index_sequence in Modern C++
Introduced in C++14, std::index_sequence is a
compile-time utility that represents a sequence of integers as a type.
Its primary purpose is to assist in expanding variadic parameter packs
of arbitrary length, specifically when operations require positional
indexing into containers like std::tuple, arrays, or other
heterogeneous collections. By converting a size into a compile-time pack
of numeric indices, std::index_sequence bridges the gap
between indexed access and template pack expansion.
The Problem with Parameter Packs
Variadic templates allow functions and classes to accept an arbitrary
number of template arguments. However, C++ does not provide a built-in
syntax to directly index into a parameter pack (such as
args[i]). While pack expansion allows expanding an entire
pack simultaneously using the ellipsis operator (...),
scenarios often arise where you must:
- Access elements of an existing structure (such as a
std::tupleorstd::array) as if they were an expanded parameter pack. - Track the numeric position of each element during expansion.
- Access multiple containers simultaneously at identical index positions.
Because std::get<I>(tuple) requires a compile-time
constant for I, a standard runtime loop cannot be used to
iterate over and unpack elements.
How
std::index_sequence Works
std::index_sequence is a specialization of
std::integer_sequence defined in the
<utility> header:
template<std::size_t... Ints>
using index_sequence = std::integer_sequence<std::size_t, Ints...>;It carries no runtime state or data; its value lies entirely in its
type signature, which embeds a sequence of std::size_t
values.
To generate this sequence automatically without manually specifying
each number, C++ provides the helper template alias
std::make_index_sequence<N>. Supplying N
generates
std::index_sequence<0, 1, 2, ..., N - 1>.
The Helper Function Pattern
Using std::index_sequence typically follows an idiom
involving a public interface function and a private helper
implementation:
- The public function determines the number of elements \(N\) and calls the helper, passing
std::make_index_sequence<N>{}as an argument. - The helper function accepts the
std::index_sequence<Is...>tag, which allows the compiler to deduce the pack of integersIs.... - The helper expands
Is...across the desired expression.
Practical Example: Unpacking a Tuple into a Function
The canonical use case is expanding a std::tuple into
arguments for a callable object, which is the foundational design behind
C++17's std::apply:
#include <iostream>
#include <tuple>
#include <utility>
// Target function
void print_sum(int a, double b, int c) {
std::cout << a + b + c << '\n';
}
// Helper implementation
template <typename Function, typename Tuple, std::size_t... Is>
void apply_impl(Function&& func, Tuple&& t, std::index_sequence<Is...>) {
// Is... expands to 0, 1, 2, enabling indexed pack expansion
func(std::get<Is>(std::forward<Tuple>(t))...);
}
// Public interface
template <typename Function, typename Tuple>
void apply(Function&& func, Tuple&& t) {
constexpr auto size = std::tuple_size_v<std::decay_t<Tuple>>;
apply_impl(std::forward<Function>(func),
std::forward<Tuple>(t),
std::make_index_sequence<size>{});
}
int main() {
auto args = std::make_tuple(10, 20.5, 30);
apply(print_sum, args); // Outputs 60.5
}Core Benefits
- Compile-Time Safety: All index generation and validation happen at compile time, eliminating out-of-bounds errors and runtime overhead.
- Lockstep Expansion: It allows parallel expansion of
separate structures. By expanding
std::get<Is>(tupleA)andstd::get<Is>(tupleB)simultaneously, operations can be applied pair-wise across different packs. - Code Elimination: It eliminates the need for recursive template instantiations, which were previously the only way to peel off elements from a variadic pack in C++11. This significantly reduces compiler memory usage and speeds up build times.