Template Alchemy: Mastering Variadic Packs with TypePack (Part 3 of 8)
Part 3: Slicing and Dicing — Subsets of TypePacks

In the previous part, we learned how to measure a TypePack and pick a single element. However, when building complex template libraries, we often need more than just one type; we need to extract entire chunks of data. Whether you are stripping away metadata or isolating a specific range of arguments, "slicing" is a critical skill.
In this article, we will implement first_n_t, skip_n_t, and the versatile subpack_t.
Expanding the TypePack Interface
We add three new alias templates to our core TypePack structure. These act as the public API for our slicing operations.
template <class... Ts>
struct TypePack : std::type_identity<TypePack<Ts...>> {
static constexpr size_t size = sizeof...(Ts);
// ... previous definitions ...
template <size_t Size>
using first_n_t = details::TypePackFirstN<Size, TypePack>::type;
template <size_t Size>
using skip_n_t = details::TypePackSkipN<Size, TypePack>::type;
template <size_t Index, size_t Size>
using subpack_t = details::TypePackSubPack<Index, Size, TypePack>::type;
};
The Supporting Machinery
To implement these, we need helper utilities to rebuild packs. The most basic operations are inserting a type at the beginning or the end of an existing TypePack.
1. Internal Helpers: Insertion
template <class T, class Pack>
struct TypePackInsertAtFirstPosition;
template <class T, class... Ts>
struct TypePackInsertAtFirstPosition<T, TypePack<Ts...>>
: std::type_identity<TypePack<T, Ts...>> {};
2. Taking the First N Types
To get the first N types, we use recursion. We "take" the head of the pack and prepend it to the result of taking N-1 types from the tail.
template <size_t N, class Pack>
struct TypePackFirstN;
template <size_t N>
requires (N != 0)
struct TypePackFirstN<N, TypePack<>> {
static_assert(AlwaysFalse<std::integral_constant<size_t, N>>, "N is out of range");
};
template <class...Ts>
struct TypePackFirstN<0, TypePack<Ts...>> : std::type_identity<TypePack<>> {
};
template <size_t N, class T, class...Ts>
requires (N != 0)
struct TypePackFirstN<N, TypePack<T, Ts...>> :
TypePackInsertAtFirstPosition<T, typename TypePackFirstN<N - 1, TypePack<Ts...>>::type> {
};
3. Skipping N Types
Skipping is simpler than taking. We don't need to rebuild the pack; we simply discard the head until N reaches zero.
template <size_t N, class Pack>
struct TypePackSkipN;
template <size_t N>
requires (N != 0)
struct TypePackSkipN<Size, TypePack<>> {
static_assert(AlwaysFalse<std::integral_constant<size_t, N>>, "N is out of range");
};
template <class...Ts>
struct TypePackSkipN<0, TypePack<Ts...>> : std::type_identity<TypePack<Ts...>> {
};
template <size_t N, class T, class...Ts>
requires (N != 0)
struct TypePackSkipN<N, TypePack<T, Ts...>> : TypePackSkipN<N - 1, TypePack<Ts...>> {
};
4. The General Subpack (Slice)
The subpack_t operation is a beautiful example of composition. To get a range starting at Index with a specific Size, we first skip the prefix and then take the requested number of elements from the remainder.
template <size_t Index, size_t Size, class Pack>
struct TypePackSubPack;
template <size_t Index, size_t Size, class...Ts>
requires (Index > sizeof...(Ts) || Size > sizeof...(Ts) || Index + Size > sizeof...(Ts))
struct TypePackSubPack<Index, Size, TypePack<Ts...>> {
static_assert(Index <= sizeof...(Ts), "Index out of range");
static_assert(Size <= sizeof...(Ts), "Size out of range");
static_assert(Index + Size <= sizeof...(Ts), "Index + Size out of range");
};
template <size_t Index, size_t Size, class...Ts>
requires (Index <= sizeof...(Ts) && Size <= sizeof...(Ts) && Index + Size <= sizeof...(Ts))
struct TypePackSubPack<Index, Size, TypePack<Ts...>> :
TypePackFirstN<Size, typename TypePackSkipN<Index, TypePack<Ts...>>::type> {
};
Validation with static_assert
Our tests ensure that slicing works on empty packs, single-element packs, and larger collections.
TEST(TypePackTests, FirstN0)
{
using Pack = TypePack<>;
static_assert(std::is_same_v<Pack::first_n_t<0>, TypePack<>>);
}
TEST(TypePackTests, FirstN1)
{
using Pack = TypePack<int>;
static_assert(std::is_same_v<Pack::first_n_t<0>, TypePack<>>);
static_assert(std::is_same_v<Pack::first_n_t<1>, TypePack<int>>);
}
TEST(TypePackTests, FirstN)
{
using Pack = TypePack<int, long, double, char>;
static_assert(std::is_same_v<Pack::first_n_t<0>, TypePack<>>);
static_assert(std::is_same_v<Pack::first_n_t<1>, TypePack<int>>);
static_assert(std::is_same_v<Pack::first_n_t<2>, TypePack<int, long>>);
static_assert(std::is_same_v<Pack::first_n_t<3>, TypePack<int, long, double>>);
static_assert(std::is_same_v<Pack::first_n_t<4>, TypePack<int, long, double, char>>);
}
TEST(TypePackTests, SkipN0)
{
using Pack = TypePack<>;
static_assert(std::is_same_v<Pack::skip_n_t<0>, TypePack<>>);
}
TEST(TypePackTests, SkipN1)
{
using Pack = TypePack<int>;
static_assert(std::is_same_v<Pack::skip_n_t<0>, TypePack<int>>);
static_assert(std::is_same_v<Pack::skip_n_t<1>, TypePack<>>);
}
TEST(TypePackTests, SkipN)
{
using Pack = TypePack<int, long, double, char>;
static_assert(std::is_same_v<Pack::skip_n_t<0>, TypePack<int, long, double, char>>);
static_assert(std::is_same_v<Pack::skip_n_t<1>, TypePack<long, double, char>>);
static_assert(std::is_same_v<Pack::skip_n_t<2>, TypePack<double, char>>);
static_assert(std::is_same_v<Pack::skip_n_t<3>, TypePack<char>>);
static_assert(std::is_same_v<Pack::skip_n_t<4>, TypePack<>>);
}
TEST(TypePackTests, SubPack0)
{
using Pack = TypePack<>;
static_assert(std::is_same_v<Pack::subpack_t<0, 0>, TypePack<>>);
}
TEST(TypePackTests, SubPack1)
{
using Pack = TypePack<int>;
static_assert(std::is_same_v<Pack::subpack_t<0, 0>, TypePack<>>);
static_assert(std::is_same_v<Pack::subpack_t<0, 1>, TypePack<int>>);
static_assert(std::is_same_v<Pack::subpack_t<1, 0>, TypePack<>>);
}
TEST(TypePackTests, SubPack)
{
using Pack = TypePack<int, long, double, char>;
static_assert(std::is_same_v<Pack::subpack_t<0, 0>, TypePack<>>);
static_assert(std::is_same_v<Pack::subpack_t<0, 1>, TypePack<int>>);
static_assert(std::is_same_v<Pack::subpack_t<1, 1>, TypePack<long>>);
static_assert(std::is_same_v<Pack::subpack_t<2, 1>, TypePack<double>>);
static_assert(std::is_same_v<Pack::subpack_t<3, 1>, TypePack<char>>);
static_assert(std::is_same_v<Pack::subpack_t<0, 2>, TypePack<int, long>>);
static_assert(std::is_same_v<Pack::subpack_t<1, 2>, TypePack<long, double>>);
static_assert(std::is_same_v<Pack::subpack_t<2, 2>, TypePack<double, char>>);
static_assert(std::is_same_v<Pack::subpack_t<0, 3>, TypePack<int, long, double>>);
static_assert(std::is_same_v<Pack::subpack_t<1, 3>, TypePack<long, double, char>>);
static_assert(std::is_same_v<Pack::subpack_t<0, 4>, TypePack<int, long, double, char>>);
}
Conclusion
We have now transitioned from merely looking at types to actively reshaping our type containers. By combining recursion with basic "Head/Tail" manipulation, we created a powerful slicing engine that allows us to isolate any subset of types.
In the next part, we will explore how to grow our containers: inserting new elements into a pack and merging multiple packs into one.




