With the foundation laid, we can build out a complete toolkit. We'll explore the implementation of a few key utilities to understand the common patterns used in C++ metaprogramming: partial specialization, the strict template pattern, and modern constexpr evaluation.
Accessing the Front: The Front Utility
Goal: Get the very first type from the list, or InvalidType if the list is empty.
How It Works: We use a helper struct in our Internal namespace and partial template specialization. We provide a general template that handles the failure case (an empty list) and a more specific one that pattern-matches a non-empty list.
// --- Implementation in Internal namespace ---
namespace Internal {
// This general template is chosen if no other specialization matches (i.e., for an empty list).
template<typename... Types>
struct FrontImpl {
using Type = InvalidType;
};
// This partial specialization is chosen for any list with at least one element.
template<typename Type1, typename... Types>
struct FrontImpl<Type1, Types...> {
// It captures the first type as 'Type1' and exposes it.
using Type = Type1;
};
}
// Inside TypeList<Types...>
using Front = typename Internal::FrontImpl<Types...>::Type;
The compiler automatically selects the best-matching specialization. For TypeList<int, char>, the second version is a better match. For TypeList<>, only the first one is viable.
Adding to the List: The PushFront Utility
Goal: Create a new TypeList by adding an element to the front of an existing one.
How It Works: To ensure this operation only works on a TypeList, we use the strict template pattern. We declare a primary template but leave it incomplete. The only definition we provide is a partial specialization that exclusively accepts a TypeList.
// 1. Incomplete primary template. If a non-TypeList is passed, this is chosen,
// and compilation fails because it has no ::Type member.
template<typename List, typename Element>
struct PushFront;
// 2. The only actual implementation.
template<typename Element, typename... Elements>
struct PushFront<TypeList<Elements...>, Element> {
// Unpacks the existing types and prepends the new element.
using Type = TypeList<Element, Elements...>;
};
// 3. A convenient alias for users.
template<typename List, typename Element>
using PushFront_T = typename PushFront<List, Element>::Type;
Removing from the List: The PopBack Utility
Goal: Create a new TypeList by removing the last element from an existing one.
How It Works: This operation is more complex because we can't directly access the "last" type in a parameter pack. The solution is a recursive template. It works by recursively peeling off the first element (Head), processing the rest of the list (Tail), and then adding the Head back to the front of the result. When the recursion reaches the base case—a list with just one element—it returns an empty list, effectively "dropping" the original last element. The implementation details are hidden in an Internal namespace, which is a common C++ practice.
// --- Implementation Details ---
namespace Internal {
// Primary template for the implementation (forward declaration).
template<typename List>
struct PopBackImpl;
// The recursive step:
// Peels off Head, calls PopBackImpl on the Tail, and reconstructs the list.
template<typename Head, typename... Tail>
struct PopBackImpl<TypeList<Head, Tail...>> {
using Type = PushFront_T<typename PopBackImpl<TypeList<Tail...>>::Type, Head>;
};
// Base case: When only one element is left, the result is an empty list.
template<typename Last>
struct PopBackImpl<TypeList<Last>> {
using Type = TypeList<>;
};
// Edge case: Popping from an empty list results in an empty list.
template<>
struct PopBackImpl<TypeList<>> {
using Type = TypeList<>;
};
} // namespace Internal
// The public-facing struct that users interact with.
template<typename List>
struct PopBack {
using Type = typename Internal::PopBackImpl<List>::Type;
};
// A convenient alias for users.
template<typename List>
using PopBack_T = typename PopBack<List>::Type;
Finding a Type: The IndexOf Utility
Goal: Find the compile-time index of a type T in the list.
How It Works: Instead of complex template recursion, we can use a modern constexpr immediately-invoked lambda expression (IIFE). This is easier to read and write.
// If found returns first occurence of a type in the list
// If not found returns NPos
template <typename T>
static constexpr std::size_t IndexOf = [] {
// Create a compile-time array of booleans.
constexpr std::array<bool, sizeof...(Types)> matches{ std::is_same_v<T, Types>... };
// Loop over the array at compile time to find the first 'true'.
for (std::size_t i = 0; i < matches.size(); ++i) {
if (matches[i])
return i; // found
}
return NPos; // not found
}();
This entire function runs during compilation, baking the resulting index directly into your code as a constant.
The Full Toolkit
Using these same techniques—partial specialization, recursion, if constexpr, and pack expansion—the full library also provides Back, Get<N>, Contains<T>, PushBack_T, PopFront_T, PopBack_T, Concat_T, Transform_T, Reverse_T, and Filter_T, giving you a complete set of operations for any compile-time task.