How std::make_index_sequence Works in C++
This article provides a technical overview of how
std::make_index_sequence generates compile-time integer
sequences in modern C++. It examines the underlying template structures,
explores both linear and logarithmic recursive implementation patterns,
and explains how modern compilers optimize these instantiations to avoid
reaching recursion limits.
The Role of
std::index_sequence
In C++14, the standard library introduced
std::index_sequence alongside the helper alias
std::make_index_sequence. The primary purpose of these
utilities is to unpack tuples or expand parameter packs by numerical
index.
At its core, std::index_sequence is an empty
struct that holds a variadic pack of non-type template
parameters:
template <std::size_t... Ints>
struct index_sequence {};The alias std::make_index_sequence<N> resolves to
an instantiation of
std::index_sequence<0, 1, 2, ..., N - 1>. Because
non-type template parameter packs cannot be generated using simple
runtime loops, the compiler must construct this pack using template
metaprogramming techniques.
Linear Recursive Implementation
The traditional approach to constructing an index pack relies on
recursive class template specialization. The algorithm begins at
N and recursively decrements the counter, appending each
value to an expanding parameter pack until reaching the base case of
0.
// Forward declaration
template <std::size_t N, std::size_t... Next>
struct make_index_sequence_helper
: make_index_sequence_helper<N - 1, N - 1, Next...> {};
// Base case specialization
template <std::size_t... Next>
struct make_index_sequence_helper<0, Next...> {
using type = std::index_sequence<Next...>;
};
// Public alias
template <std::size_t N>
using custom_make_index_sequence = typename make_index_sequence_helper<N>::type;When evaluating custom_make_index_sequence<3>:
make_index_sequence_helper<3>inherits frommake_index_sequence_helper<2, 2>make_index_sequence_helper<2, 2>inherits frommake_index_sequence_helper<1, 1, 2>make_index_sequence_helper<1, 1, 2>inherits frommake_index_sequence_helper<0, 0, 1, 2>- The base case
make_index_sequence_helper<0, 0, 1, 2>matches, producingstd::index_sequence<0, 1, 2>as itstype.
While this linear method is straightforward, it incurs \(O(N)\) template instantiations. For large values of \(N\), this quickly exceeds standard compiler limits (typically configured between 256 and 1024 recursive levels) and causes compilation errors.
Logarithmic Recursive Implementation
To prevent recursion depth overflow and reduce compilation overhead, standard library implementations historically used a divide-and-conquer approach. By splitting the generation in half, depth is reduced to \(O(\log N)\).
This technique combines two smaller sequences into a larger sequence through a merge operation:
// Merge two index sequences, shifting the second pack by sizeof...(I)
template <typename Seq1, typename Seq2>
struct merge_sequences;
template <std::size_t... I1, std::size_t... I2>
struct merge_sequences<std::index_sequence<I1...>, std::index_sequence<I2...>> {
using type = std::index_sequence<I1..., (sizeof...(I1) + I2)...>;
};
// Recursive definition
template <std::size_t N>
struct make_index_sequence_impl {
using type = typename merge_sequences<
typename make_index_sequence_impl<N / 2>::type,
typename make_index_sequence_impl<N - N / 2>::type
>::type;
};
// Base cases
template <>
struct make_index_sequence_impl<0> {
using type = std::index_sequence<>;
};
template <>
struct make_index_sequence_impl<1> {
using type = std::index_sequence<0>;
};
template <std::size_t N>
using fast_make_index_sequence = typename make_index_sequence_impl<N>::type;When fast_make_index_sequence<4> is
instantiated:
make_index_sequence_impl<4>splits into two instances ofmake_index_sequence_impl<2>.- Each
make_index_sequence_impl<2>evaluatesmerge_sequences<std::index_sequence<0>, std::index_sequence<0>>, which offsets the second pack to yieldstd::index_sequence<0, 1>. - The root
merge_sequencescombinesstd::index_sequence<0, 1>andstd::index_sequence<0, 1>, shifting the second sequence by2to producestd::index_sequence<0, 1, 2, 3>.
Compiler Intrinsics in Modern Toolchains
While logarithmic recursion solves depth limits in standard C++, it still creates an overhead of intermediate class template instantiations. Because parameter pack generation is a critical operation for tuple manipulation and serialization libraries, modern C++ compilers bypass recursive templates entirely.
Current versions of GCC, Clang, and MSVC provide intrinsic built-ins
such as __make_integer_seq or
__integer_pack:
template <typename T, T N>
using make_integer_sequence = __make_integer_seq<std::integer_sequence, T, N>;
template <std::size_t N>
using make_index_sequence = make_integer_sequence<std::size_t, N>;These compiler built-ins generate the integer pack directly inside the compiler's abstract syntax tree (AST) in \(O(1)\) instantiation time, entirely avoiding the template instantiation recursion stack while retaining full compatibility with standard pack expansion syntax.