Skip to main content

Command Palette

Search for a command to run...

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

Part 6: Transformation - Removing or Replacing a Specific Type

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

In the previous chapters, we focused on "positional" operations: doing things based on indices and ranges. However, in real-world metaprogramming, we often care about what a type is, not just where it is. For example, you might want to strip all void types from a pack or replace float with double for higher precision across a whole interface.

In this article, we will implement type-based filtering and transformation using remove_t and replace_t.

Advancing the Interface

We add two more aliases to our TypePack. These allow us to perform "search-and-destroy" or "search-and-replace" operations across the entire collection of types.

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

    // Remove all occurrences of type T
    template <class T>
    using remove_t = details::TypePackRemove<T, TypePack>::type;

    // Replace all occurrences of T with TReplacement
    template <class T, class TReplacement>
    using replace_t = details::TypePackReplace<T, TReplacement, TypePack>::type;
};

Type-Based Removal

The TypePackRemove utility uses recursive pattern matching to scan the pack. Unlike positional removal, this operation visits every element and decides whether to keep it or discard it.

The Logic:

  1. Base Case: Removing from an empty pack results in an empty pack.

  2. Match Case: If the head of the pack matches the target type T, we discard it and continue with the rest of the pack.

  3. No-Match Case: If the head doesn't match, we keep it (using TypePackInsertAtFirstPosition) and recurse into the tail.

template <class T, class Pack>
struct TypePackRemove;

template <class T>
struct TypePackRemove<T, TypePack<>> : std::type_identity<TypePack<>> {
};

template <class T, class...Ts>
struct TypePackRemove<T, TypePack<T, Ts...>> : TypePackRemove<T, TypePack<Ts...>> {
};

template <class T, class TFirst, class...Ts>
struct TypePackRemove<T, TypePack<TFirst, Ts...>> :
    TypePackInsertAtFirstPosition<TFirst, typename TypePackRemove<T, TypePack<Ts...>>::type> {
};

Universal Replacement

TypePackReplace follows a similar recursive structure but with a twist: instead of discarding a matching type, we swap it for a new one.

The Logic:

  • If Head == Target, the new pack starts with Replacement.

  • If Head != Target, the new pack starts with the original Head.

  • Repeat until the pack is empty.

template <class T, class TReplacement, class Pack>
struct TypePackReplace;

template <class T, class TReplacement>
struct TypePackReplace<T, TReplacement, TypePack<>> : std::type_identity<TypePack<>> {
};

template <class T, class TReplacement, class...Ts>
struct TypePackReplace<T, TReplacement, TypePack<T, Ts...>> :
    TypePackInsertAtFirstPosition<TReplacement, typename TypePackReplace<T, TReplacement, TypePack<Ts...>>::type> {
};

template <class T, class TReplacement, class TFirst, class...Ts>
struct TypePackReplace<T, TReplacement, TypePack<TFirst, Ts...>> :
    TypePackInsertAtFirstPosition<TFirst, typename TypePackReplace<T, TReplacement, TypePack<Ts...>>::type> {
};

Validation with static_assert

Our tests demonstrate that these operations work globally—affecting multiple occurrences of the same type throughout the pack.

TEST(TypePackTests, RemoveType)
{
    using Pack = TypePack<int, long, int, double, int>;

    // Removes ALL 'int' instances
    using Result = Pack::remove_t<int>;
    static_assert(std::is_same_v<Result, TypePack<long, double>>);

    // If the type isn't there, the pack remains unchanged
    static_assert(std::is_same_v<Pack::remove_t<char>, Pack>);
}

TEST(TypePackTests, ReplaceType)
{
    using Pack = TypePack<int, long, int>;

    // Replaces ALL 'int' with 'unsigned'
    using Result = Pack::replace_t<int, unsigned>;
    static_assert(std::is_same_v<Result, TypePack<unsigned, long, unsigned>>);
}

Conclusion

By moving from positional to type-based manipulation, we've given our TypePack the ability to "understand" its contents. These recursive patterns are the engine behind powerful library features, such as filtering out unsupported types or automatically upgrading types in a template-heavy API.

In the next part, we will look at how to handle nested structures by implementing an operation to flatten a pack