Skip to main content

Command Palette

Search for a command to run...

Template Alchemy: Mastering Variadic Packs with TypePack (Part 1 of 8)

Part 1: The Anatomy of Type Containers

Updated
3 min read
Template Alchemy: Mastering Variadic Packs with TypePack (Part 1 of 8)

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.