Template Alchemy: Mastering Variadic Packs with TypePack (Part 1 of 8)
Part 1: The Anatomy of Type Containers

In modern C++, variadic templates allow us to work with an arbitrary number of template arguments. However, anyone who has delved deep into Template Metaprogramming (TMP) knows that "parameter packs" (the Ts... in a template) are quite elusive. They aren't objects, they aren't types, and they aren't quite arrays. They are a language construct that can only be expanded in specific contexts.
To harness the power of these packs, we need a way to "capture" them, move them around, and manipulate them without immediate expansion. This is where TypePack comes in.
The Problem: Why Can't We Just Use Packs?
A pack of types is not a first-class citizen in C++. You cannot store a pack in a variable, nor can you create a using alias for a raw pack.
Consider the following illegal code:
template <typename... Ts>
struct Error {
using MyPack = Ts...; // ERROR: A parameter pack cannot be used as an alias
};
Because of this limitation, if you want to pass a collection of types to another metafunction or store them for later use, you must wrap them in a container.
The std::tuple Example
The most common example of a pack container in the Standard Library is std::tuple.
// std::tuple "captures" the pack Ts into a single type
std::tuple<int, double, char> myData;
While std::tuple is great for storing values of different types, it carries overhead because it’s a concrete class designed for runtime use. For pure compile-time metaprogramming, we need something lighter—a "type-only" container.
The Implementation: TypePack
TypePack is a minimalist structure designed to hold a pack of types. It inherits from std::type_identity to make it easier to refer to the pack's own type in complex transformations.
#include <type_traits>
template <class... Ts>
struct TypePack : std::type_identity<TypePack<Ts...>> {
// This structure intentionally has no data members.
// Its only purpose is to carry the pack 'Ts...' in its signature.
};
By wrapping Ts... inside TypePack, the pack becomes part of a single, concrete type. You can now pass this TypePack to other templates, nest it, or return it from a metafunction.
Validation and Examples
To ensure our TypePack behaves as expected, we can use static_assert. These tests verify that different instances of TypePack are recognized as unique types and that they correctly inherit their own identity.
// 1. Verify that empty packs are valid
using EmptyPack = TypePack<>;
static_assert(!std::is_same_v<EmptyPack, TypePack<int>>);
// 2. Verify identity inheritance
using MyTypes = TypePack<int, float, double>;
static_assert(std::is_same_v<MyTypes::type, MyTypes>);
// 3. Nested TypePacks (TypePacks can hold other TypePacks)
using Nested = TypePack<TypePack<int, int>, char>;
static_assert(std::is_same_v<Nested::type, TypePack<TypePack<int, int>, char>>);
Conclusion
TypePack is the foundation of our library. By wrapping a variadic pack into a struct, we bypass the language limitations that prevent us from treating packs as entities. We have moved from a raw, "unstable" pack to a stable, nameable type.
In the next part, we will explore how to calculate the size of a TypePack and how to get individual types from it.




