Template Alchemy: Mastering Variadic Packs with TypePack (Part 4 of 8)
Part 4: Growing the Pack — Insertion and Merging

In the previous parts, we learned how to encapsulate a variadic pack and how to slice it into smaller pieces. However, a truly flexible meta-container must also be able to grow. In this article, we will implement the ability to merge multiple TypePack instances and insert new types at any arbitrary position.
Expanding the Interface
We introduce two powerful aliases to the TypePack struct: concat_t for merging and insert_at_t for precise placement.
template <class... Ts>
struct TypePack : std::type_identity<TypePack<Ts...>> {
// ... previous definitions (size, element_t, etc.) ...
// Merge this pack with any number of other TypePacks
template <class... Packs>
using concat_t = details::TypePackConcat<TypePack, Packs...>::type;
// Insert a single type T at a specific Index
template <size_t Index, class T>
using insert_at_t = details::TypePackInsertAt<T, Index, TypePack>::type;
};
Merging Packs: The TypePackConcat Logic
To ensure type safety, we use the IsSpecializationOf concept. This prevents the user from accidentally trying to concatenate a TypePack with a std::tuple or other unrelated types.
The Implementation
Concatenation is achieved by recursively expanding the internal packs and folding them into a single TypePack.
template <IsSpecializationOf<TypePack>...Packs>
struct TypePackConcat;
template <>
struct TypePackConcat<> : std::type_identity<TypePack<>> {
};
template <class...Ts>
struct TypePackConcat<TypePack<Ts...>> : std::type_identity<TypePack<Ts...>> {
};
template <class...Ts, class...Ts2, IsSpecializationOf<TypePack>...Packs>
struct TypePackConcat<TypePack<Ts...>, TypePack<Ts2...>, Packs...> :
TypePackConcat<TypePack<Ts..., Ts2...>, Packs...> {
};
This recursive approach allows us to pass an unlimited number of packs to concat_t, flattening them all into one flat structure.
Precise Insertion: The TypePackInsertAt Logic
Inserting an element at an arbitrary index is a perfect demonstration of the "Slicing" tools we built in Part 3. Instead of writing a complex new recursion, we simply:
Slice the pack into two halves (before and after the index).
Concatenate the first half, the new type, and the second half.
template <class T, size_t Index, class Pack>
struct TypePackInsertAt;
template <class T, size_t Index, class...Ts>
requires (Index > sizeof...(Ts))
struct TypePackInsertAt<T, Index, TypePack<Ts...>> {
static_assert(AlwaysFalse<std::integral_constant<size_t, Index>>, "Index out of range");
};
template <class T, size_t Index, class...Ts>
requires (Index <= sizeof...(Ts))
struct TypePackInsertAt<T, Index, TypePack<Ts...>> :
TypePackConcat<
typename TypePackFirstN<Index, TypePack<Ts...>>::type,
TypePack<T>,
typename TypePackSkipN<Index, TypePack<Ts...>>::type> {
};
This "Lego-block" approach to metaprogramming makes the code significantly easier to maintain and reason about.
Validation with static_assert
Our tests verify that concatenation handles empty packs correctly and that insertion works at the boundaries (index 0 and index size).
TEST(TypePackTests, Concat)
{
using Pack0 = TypePack<>;
using Pack1 = TypePack<float>;
using Pack2 = TypePack<unsigned, char8_t>;
using Pack4 = TypePack<int, long, double, char>;
static_assert(std::is_same_v<Pack0::concat_t<Pack0>, TypePack<>>);
static_assert(std::is_same_v<Pack0::concat_t<Pack1>, TypePack<float>>);
static_assert(std::is_same_v<Pack0::concat_t<Pack2>, TypePack<unsigned, char8_t>>);
static_assert(std::is_same_v<Pack0::concat_t<Pack4>, TypePack<int, long, double, char>>);
static_assert(std::is_same_v<Pack1::concat_t<Pack0>, TypePack<float>>);
static_assert(std::is_same_v<Pack1::concat_t<Pack1>, TypePack<float, float>>);
static_assert(std::is_same_v<Pack1::concat_t<Pack2>, TypePack<float, unsigned, char8_t>>);
static_assert(std::is_same_v<Pack1::concat_t<Pack4>, TypePack<float, int, long, double, char>>);
static_assert(std::is_same_v<Pack2::concat_t<Pack0>, TypePack<unsigned, char8_t>>);
static_assert(std::is_same_v<Pack2::concat_t<Pack1>, TypePack<unsigned, char8_t, float>>);
static_assert(std::is_same_v<Pack2::concat_t<Pack2>, TypePack<unsigned, char8_t, unsigned, char8_t>>);
static_assert(std::is_same_v<Pack2::concat_t<Pack4>, TypePack<unsigned, char8_t, int, long, double, char>>);
static_assert(std::is_same_v<Pack4::concat_t<Pack0>, TypePack<int, long, double, char>>);
static_assert(std::is_same_v<Pack4::concat_t<Pack1>, TypePack<int, long, double, char, float>>);
static_assert(std::is_same_v<Pack4::concat_t<Pack2>, TypePack<int, long, double, char, unsigned, char8_t>>);
static_assert(std::is_same_v<Pack4::concat_t<Pack4>, TypePack<int, long, double, char, int, long, double, char>>);
}
TEST(TypePackTests, InsertAt)
{
using Pack = TypePack<int, long, double, char>;
static_assert(std::is_same_v<Pack::insert_at_t<0, float>, TypePack<float, int, long, double, char>>);
static_assert(std::is_same_v<Pack::insert_at_t<1, float>, TypePack<int, float, long, double, char>>);
static_assert(std::is_same_v<Pack::insert_at_t<2, float>, TypePack<int, long, float, double, char>>);
static_assert(std::is_same_v<Pack::insert_at_t<3, float>, TypePack<int, long, double, float, char>>);
static_assert(std::is_same_v<Pack::insert_at_t<4, float>, TypePack<int, long, double, char, float>>);
}
Conclusion
With the addition of concatenation and insertion, our TypePack is no longer a static snapshot of types. It is now a dynamic structure that can be merged, extended, and rebuilt at will. We have successfully moved from basic introspection to active structural transformation.
In the next part, we will explore how to shrink our containers: removing specific elements and deleting entire ranges from the pack.




