Skip to main content

Command Palette

Search for a command to run...

Template Alchemy: Mastering Variadic Packs with TypePack

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

Updated
4 min read
Template Alchemy: Mastering Variadic Packs with TypePack

In the previous chapters, we treated our TypePack primarily as a linear sequence—a list that can be sliced, grown, and flattened. However, in many metaprogramming scenarios, a parameter pack acts more like a Set.

You might need to know if a specific interface exists in a collection, find the specific index of a type to access a corresponding value in a std::tuple, or strip away redundant duplicates to create a canonical list of types.

In this article, we will implement three crucial set-operations: contains (existence), index_of (lookup), and unique (deduplication).

1. Existence: The contains Check

Checking if a type exists within a pack used to require complex recursive inheritance or std::disjunction. With C++17, this becomes a trivial one-liner using Fold Expressions.

We add a static constexpr boolean to our main structure. This allows users to write Pack::contains<int> effectively.

template <class... Ts>
struct TypePack : std::type_identity<TypePack<Ts...>> {
    // ... previous code ...

    // C++17 Fold Expression to check for type existence
    template <class T>
    static constexpr bool contains = (std::is_same_v<T, Ts> || ...);

    // ...
};

2. Location: The index_of Operation

Knowing a type exists is often only half the battle. If you are building a storage system (like a Variant or a Tuple wrapper), you often need the numerical index of that type to access memory.

Since parameter packs cannot be indexed at runtime, we perform a compile-time linear search. We recurse through the pack:

  1. Found: Return 0.

  2. Not Found: Return the result of the recursion + 1.

  3. End of Pack: Return a sentinel value (max size_t) to indicate failure.

We add this as a static member to TypePack:

template <class... Ts>
struct TypePack : std::type_identity<TypePack<Ts...>> {
    // ...
    template <class T>
    static constexpr size_t index_of = details::TypePackIndexOf<T, TypePack>::value;
};

And the implementation logic:

template <class T, class Pack>
struct TypePackIndexOf;

// Base Case: Pack is empty, type not found. Return MAX_SIZE_T.
template <class T>
struct TypePackIndexOf<T, TypePack<>> : 
    std::integral_constant<size_t, std::numeric_limits<size_t>::max()> {};

// Match Case: The head of the pack matches T. Index is 0.
template <class T, class... Ts>
struct TypePackIndexOf<T, TypePack<T, Ts...>> : 
    std::integral_constant<size_t, 0> {};

// Recursive Step: Head does not match. Check the tail.
template <class T, class TFirst, class... Ts>
struct TypePackIndexOf<T, TypePack<TFirst, Ts...>> :
    std::integral_constant<size_t,
        TypePackIndexOf<T, TypePack<Ts...>>::value == std::numeric_limits<size_t>::max() 
            ? std::numeric_limits<size_t>::max() 
            : TypePackIndexOf<T, TypePack<Ts...>>::value + 1
    > {};

3. Uniqueness: The unique Transformation

When automatically generating types (e.g., deducing return types from a set of functions), you often end up with duplicates. To resolve ambiguity, you need to reduce the pack to a set of unique types.

This implementation relies on the TypePackRemove utility we built in Part 6. The strategy is "Filter and Conserve":

  1. Take the first type T.

  2. Remove all occurrences of T from the rest of the pack.

  3. Recurse on the now-filtered tail.

  4. Prepend T back to the result.

This ensures order is preserved while duplicates are eliminated.

// Public Alias
template <IsSpecializationOf<TypePack> Pack>
using type_pack_unique_t = details::TypePackUnique<Pack>::type;

// Implementation
template <class Pack>
struct TypePackUnique;

// Base Case: Empty pack is already unique
template <>
struct TypePackUnique<TypePack<>> : std::type_identity<TypePack<>> {};

// Recursive Step
template <class T, class... Ts>
struct TypePackUnique<TypePack<T, Ts...>> :
    TypePackInsertAtFirstPosition<
        T, 
        // 1. Remove T from the tail (Ts...)
        // 2. Compute Unique on that filtered tail
        typename TypePackUnique<
            typename TypePackRemove<T, TypePack<Ts...>>::type
        >::type
    > {};

Validation

We verify our set operations with a suite of tests covering existence, valid/invalid indices, and duplicate removal.

C++

// 1. Testing Contains
TEST(TypePackTests, Contains)
{
    using Pack = TypePack<int, long, double, char>;
    static_assert(Pack::contains<int>);
    static_assert(Pack::contains<double>);
    static_assert(!Pack::contains<char8_t>); // Not in pack

    // Edge case: Empty pack
    static_assert(!TypePack<>::contains<int>);
}

// 2. Testing IndexOf
TEST(TypePackTests, IndexOf)
{
    using Pack = TypePack<int, long, double, char>;

    static_assert(Pack::index_of<int> == 0);
    static_assert(Pack::index_of<double> == 2);
    static_assert(Pack::index_of<char> == 3);

    // Not found returns MAX
    static_assert(Pack::index_of<char8_t> == std::numeric_limits<size_t>::max());
    static_assert(TypePack<>::index_of<int> == std::numeric_limits<size_t>::max());
}

// 3. Testing Unique
TEST(TypePackTests, Unique)
{
    // Input contains duplicates of int, long, and char
    using Pack = TypePack<int, long, int, char, long, long, char, char8_t>;
    using Expected = TypePack<int, long, char, char8_t>;

    static_assert(std::is_same_v<type_pack_unique_t<Pack>, Expected>);

    // Already unique packs remain unchanged
    static_assert(std::is_same_v<type_pack_unique_t<TypePack<int>>, TypePack<int>>);
    static_assert(std::is_same_v<type_pack_unique_t<TypePack<>>, TypePack<>>);
}

Conclusion

With the addition of contains, index_of, and unique, our TypePack has evolved into a fully functional compile-time container. We can now treat variadic templates not just as a list of arguments, but as a searchable, indexable, and distinct set of types.

You can explore the full source code for the library on GitHub, including the implementation details and the comprehensive test suite.

This concludes our deep dive into the core implementation of TypePack. By combining structural manipulation (Concat, Slice) with introspective logic (Find, Unique), we have built a library capable of handling complex C++ metaprogramming tasks with clean, readable syntax.