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

Search for a command to run...
Part 3: Slicing and Dicing — Subsets of TypePacks

No comments yet. Be the first to comment.
If you have a Linux server at home or in a small office, chances are you also have Windows machines that need to access files on it. The traditional path — installing and configuring Samba by hand — i

Part 8: Set Semantics — Existence, Indexing, and Uniqueness

Part 7: Flattening — Collapsing Nested TypePacks

Part 6: Transformation - Removing or Replacing a Specific Type

Part 5: Refining the Pack — Deletion and Range Removal In the previous chapters, we focused on building and expanding our TypePack. However, effective type manipulation often requires surgical precision in removing unwanted elements. Whether you're s...

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.
TypePack InterfaceWe 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;
};
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.
template <class T, class Pack>
struct TypePackInsertAtFirstPosition;
template <class T, class... Ts>
struct TypePackInsertAtFirstPosition<T, TypePack<Ts...>>
: std::type_identity<TypePack<T, Ts...>> {};
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> {
};
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...>> {
};
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> {
};
static_assertOur 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>>);
}
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.