Variadic Template Data Structures in C++: Complete Guide with Examples

Variadic template data structures in C++

Variadic template data structures in C++

The first time I tried to build a type-safe container that could hold “any number of any types,” I hit a wall with regular C++ templates. A template<typename T> class can only ever hold one type. If I wanted a tuple-like structure that stored an int, a string, and a double together, plain templates simply couldn’t express that. That’s the exact problem variadic templates were introduced to solve in C++11, and they’ve since become the backbone of some of the most important data structures in modern C++ — std::tuple, std::variant, and countless custom containers I’ve written for real projects.

In this guide, I’m going to walk through variadic templates from the ground up, focused specifically on building data structures with them. I’ll cover the syntax, how the compiler actually expands these templates behind the scenes, and then build several complete, working data structures — a generic tuple, a type-safe heterogeneous list, and a compile-time array wrapper. Along the way, I’ll point out the mistakes I made when I was learning this, and the practices that saved me time later.

What Are Variadic Templates?

A variadic template is a template that accepts a variable number of arguments, of potentially different types. The syntax uses an ellipsis (...) to denote a “parameter pack” — a collection of template parameters treated as a single entity until you “unpack” it.

template<typename... Args>
class MyContainer {
    // Args represents zero or more types
};

Here, Args is a template parameter pack. It’s not a type itself — it’s a placeholder for a list of types. You can instantiate MyContainer with zero, one, or a hundred type arguments:

MyContainer<> empty;
MyContainer<int> single;
MyContainer<int, double, std::string> multiple;

There are two kinds of packs you’ll deal with constantly:

  1. Template parameter packstypename... Args
  2. Function parameter packsArgs... args (the actual values)

The number of elements in a pack can be queried with the sizeof...() operator, which is evaluated entirely at compile time:

template<typename... Args>
void countArgs(Args... args) {
    std::cout << "Argument count: " << sizeof...(args) << "\n";
}

Why Data Structures Need Variadic Templates

Before C++11, building a heterogeneous container meant one of two unpleasant options: use void* and lose type safety, or hand-write a separate class for every possible combination of types (a Pair<T1,T2>, a Triple<T1,T2,T3>, and so on, forever). Variadic templates collapse all of that into a single, recursive definition. This is exactly how std::tuple is implemented in every major standard library, and it’s the pattern I’ll build from scratch below so the internal mechanics are visible rather than hidden behind <tuple>.

The Core Technique: Recursive Inheritance

The most common way to build a variadic data structure is recursive template instantiation, where the compiler generates a chain of classes, each holding one element and inheriting from (or containing) the “rest” of the pack.

Let’s build a minimal tuple step by step.

Step 1: The Base Case and Recursive Case

#include <iostream>

// Primary template - handles the recursive case
template<typename... Types>
class MyTuple;

// Base case: empty tuple
template<>
class MyTuple<> {
public:
    // Nothing to store
};

// Recursive case: peel off the first type, recurse on the rest
template<typename Head, typename... Tail>
class MyTuple<Head, Tail...> : private MyTuple<Tail...> {
public:
    MyTuple() = default;

    MyTuple(const Head& head, const Tail&... tail)
        : MyTuple<Tail...>(tail...), value(head) {}

    Head value;

    MyTuple<Tail...>& rest() {
        return *this;
    }
};

int main() {
    MyTuple<int, double, std::string> t(42, 3.14, "hello");

    std::cout << t.value << "\n";                 // 42
    std::cout << t.rest().value << "\n";           // 3.14
    std::cout << t.rest().rest().value << "\n";    // hello

    return 0;
}

Output:

42
3.14
hello

How the Compiler Expands This

This is the part that confused me most when I started, so it’s worth being explicit. When you write MyTuple<int, double, std::string>, the compiler doesn’t generate one class — it generates a chain:

MyTuple<int, double, std::string>
  → inherits from MyTuple<double, std::string>
      → inherits from MyTuple<std::string>
          → inherits from MyTuple<>

Each level in this chain stores exactly one value member of its own Head type, and privately inherits the rest. The base-case specialization MyTuple<> terminates the recursion — without it, the compiler would try to instantiate an infinite chain and fail with a “template instantiation depth exceeded” error. This base case is non-negotiable in every variadic recursive structure; forgetting it is the single most common variadic template compile error.

Indexed Access: get<N>()

A raw chain of value and rest() calls isn’t very usable. Real tuple implementations provide std::get<N>(tuple) for indexed access. Let’s add that using a recursive helper struct.

#include <iostream>

template<typename... Types>
class MyTuple;

template<>
class MyTuple<> {};

template<typename Head, typename... Tail>
class MyTuple<Head, Tail...> : public MyTuple<Tail...> {
public:
    MyTuple() = default;
    MyTuple(const Head& head, const Tail&... tail)
        : MyTuple<Tail...>(tail...), value(head) {}

    Head value;
};

// Helper: recursively find the type/value at index N
template<size_t N, typename T>
struct TupleGetter;

// Base case: index 0 refers to the current level's value
template<typename Head, typename... Tail>
struct TupleGetter<0, MyTuple<Head, Tail...>> {
    static Head& get(MyTuple<Head, Tail...>& t) {
        return t.value;
    }
};

// Recursive case: strip one level, decrement N
template<size_t N, typename Head, typename... Tail>
struct TupleGetter<N, MyTuple<Head, Tail...>> {
    static auto get(MyTuple<Head, Tail...>& t)
        -> decltype(TupleGetter<N - 1, MyTuple<Tail...>>::get(t)) {
        return TupleGetter<N - 1, MyTuple<Tail...>>::get(t);
    }
};

template<size_t N, typename... Types>
auto get(MyTuple<Types...>& t)
    -> decltype(TupleGetter<N, MyTuple<Types...>>::get(t)) {
    return TupleGetter<N, MyTuple<Types...>>::get(t);
}

int main() {
    MyTuple<int, double, std::string> t(42, 3.14, "hello");

    std::cout << get<0>(t) << "\n";
    std::cout << get<1>(t) << "\n";
    std::cout << get<2>(t) << "\n";

    get<0>(t) = 100;
    std::cout << "Updated: " << get<0>(t) << "\n";

    return 0;
}

Output:

42
3.14
hello
Updated: 100

This is a simplified version of what libstdc++ and libc++ actually do internally (they use empty-base optimization and compressed pairs for extra efficiency, which I’ll touch on in the performance section).

A Practical Variadic Data Structure: Type-Safe Heterogeneous Stack

Tuples are fixed-size and read mostly by index. Sometimes what you actually want is a stack-like structure built at compile time, where each “push” changes the type. Here’s one built entirely with variadic templates and no runtime overhead:

#include <iostream>
#include <string>

template<typename... Elements>
class TypeStack {
public:
    static constexpr size_t size = sizeof...(Elements);

    template<typename T>
    using Push = TypeStack<T, Elements...>;
};

// Print helper using fold expression (C++17)
template<typename... Args>
void describe() {
    std::cout << "Stack has " << sizeof...(Args) << " types: ";
    ((std::cout << typeid(Args).name() << " "), ...);
    std::cout << "\n";
}

int main() {
    using Empty = TypeStack<>;
    using S1 = Empty::Push<int>;
    using S2 = S1::Push<double>;
    using S3 = S2::Push<std::string>;

    std::cout << "Empty size: " << Empty::size << "\n";
    std::cout << "S3 size: " << S3::size << "\n";

    describe<int, double, std::string>();

    return 0;
}

Output (typeid names are compiler-dependent, e.g., GCC):

Empty size: 0
S3 size: 3
Stack has 3 types: i d NSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE

This pattern — where the “container” exists purely as a type, with zero runtime footprint — is used heavily in metaprogramming libraries and compile-time state machines.

Fold Expressions (C++17): Simplifying Variadic Operations

Before C++17, unpacking a parameter pack to perform an operation on every element required recursive function templates. C++17 introduced fold expressions, which drastically simplify this.

#include <iostream>

template<typename... Args>
auto sumAll(Args... args) {
    return (args + ...);   // unary right fold
}

template<typename... Args>
void printAll(Args... args) {
    ((std::cout << args << " "), ...);  // fold over comma operator
    std::cout << "\n";
}

int main() {
    std::cout << sumAll(1, 2, 3, 4, 5) << "\n";
    printAll("Building", "with", "fold", "expressions", 2026);
    return 0;
}

Output:

15
Building with fold expressions 2026

There are four fold forms: (pack op ...), (... op pack), (pack op ... op init), and (init op ... op pack). For data structure work, the binary folds with an initial value are especially useful when the pack might be empty:

template<typename... Args>
auto sumWithDefault(Args... args) {
    return (0 + ... + args);  // safe even when Args is empty
}

Internal Working and Compiler Process

This is worth understanding deeply, because it explains both the power and the cost of variadic templates.

  1. Template instantiation, not runtime iteration. Every distinct combination of types passed to a variadic template causes the compiler to generate a separate concrete class or function. MyTuple<int, double> and MyTuple<double, int> are entirely unrelated types as far as the compiler is concerned — different vtables (if virtual functions exist), different sizes, different everything.
  2. Recursive expansion happens at compile time. When the compiler sees MyTuple<Head, Tail...>, it performs pattern matching against the parameter pack, peels off Head, and recursively instantiates MyTuple<Tail...>. This recursion has a hard limit — GCC and Clang both default to a maximum template instantiation depth (commonly 900 for GCC, adjustable via -ftemplate-depth). For a tuple of a few dozen elements this is a non-issue; for auto-generated code with thousands of types, it can matter.
  3. Name mangling reflects the full instantiation. Because each variadic instantiation is a genuinely distinct type, the mangled symbol name encodes the entire parameter pack. This is one reason template-heavy binaries can have large symbol tables and slower link times — a phenomenon often called “template bloat.”
  4. Memory layout. In the recursive-inheritance tuple I built above, each level of the hierarchy adds its own Head value member. Thanks to the empty base optimization (EBO), if any of the base classes have no data members, the compiler can overlap their storage instead of wasting a byte on each. Real implementations of std::tuple are careful to arrange this so that a tuple of N types has essentially the same memory footprint as a hand-written struct with N members — no wasted padding purely because of the variadic mechanism.
#include <iostream>
#include <tuple>

int main() {
    std::cout << "sizeof(std::tuple<int,double,char>): "
              << sizeof(std::tuple<int, double, char>) << "\n";
    std::cout << "sizeof(struct{int;double;char;}) equivalent: "
              << sizeof(int) + sizeof(double) + sizeof(char) << " (unpadded)\n";
    return 0;
}

On most 64-bit systems, this prints something like sizeof(std::tuple<int,double,char>): 24, matching (after alignment/padding) what a hand-rolled struct would need — confirming there’s no extra per-element overhead from the variadic machinery itself.

Best Practices

Performance Considerations

Variadic template data structures are, in the vast majority of cases, zero runtime overhead compared to hand-written equivalents. The recursion happens entirely at compile time; by the time you have a binary, MyTuple<int, double, std::string> is just a concrete class with three data members laid out according to normal C++ rules.

The real costs are:

Debugging Common Mistakes

Mistake 1: Missing base case

template<typename Head, typename... Tail>
class Broken {
    Broken<Tail...> rest; // no template<> class Broken<> defined anywhere
};

This fails with a recursive instantiation error once Tail... becomes empty, because there’s no matching specialization to stop at.

Mistake 2: Forgetting sizeof... vs sizeof

template<typename... Args>
void f(Args... args) {
    std::cout << sizeof(args) << "\n"; // WRONG: sizeof of the FIRST arg only if used like this incorrectly
    std::cout << sizeof...(args) << "\n"; // RIGHT: count of arguments
}

Mistake 3: Expanding a pack in the wrong context

template<typename... Args>
void g(Args... args) {
    // std::cout << args...; // ERROR: `...` here isn't a valid expansion pattern
    ((std::cout << args), ...); // Correct: fold expression
}

Mistake 4: Assuming pack order is preserved through unrelated template deduction paths. When packs interact with std::forward or nested variadic calls, double-check with static_assert(std::is_same_v<...>) that argument order survived as expected.

Real-World Applications

Interview Questions

  1. What is a parameter pack, and how does it differ from a regular template parameter?
  2. Explain how sizeof...() differs from sizeof().
  3. Walk through how the compiler instantiates a recursive variadic template like a custom tuple.
  4. Why is a base-case specialization required for recursive variadic templates?
  5. What is a fold expression, and what problem did it solve that existed before C++17?
  6. How would you implement your own simplified version of std::tuple::get<N>()?
  7. What is template/code bloat, and how do variadic templates contribute to it?
  8. How does empty base optimization (EBO) interact with variadically-inherited data structures?

FAQs

Q: Can a variadic template pack be empty? Yes. MyTuple<> is valid and typically maps to the base-case specialization with no members.

Q: Can you have multiple parameter packs in one template? A function template can deduce multiple packs if the compiler can unambiguously separate them (common in std::tuple-based factory functions), but a class template can only have one parameter pack, and it must be the last template parameter.

Q: Is there runtime overhead compared to a hand-written struct? No, in the general case. The compiler generates concrete, fixed-layout code — the “generic-ness” disappears after compilation.

Q: Do I need C++17 for variadic templates? No — variadic templates themselves are a C++11 feature. Fold expressions (which make working with them far more pleasant) require C++17.

Q: What’s the difference between Args... in the template parameter list and Args... as a function parameter? The first declares the types pack (typename... Args); the second declares the values pack (Args... args) using those types.

Troubleshooting Tips

Summary and Key Takeaways

Variadic templates turn the single-type limitation of ordinary templates into an open-ended mechanism for building data structures that handle any number of heterogeneous types — with all of the type safety and none of the runtime cost of void*-based alternatives. The core technique is recursive instantiation: peel off one type, recurse on the rest, and always provide a base case to stop the recursion. Modern C++ (17 and beyond) adds fold expressions and if constexpr, which make manipulating these packs dramatically cleaner than the SFINAE-heavy code required in C++11/14. Once you’ve built a tuple from scratch, you’ll recognize the same recursive pattern inside std::tuple, std::variant, and most serialization or event-dispatch libraries you’ll encounter.

References

Exit mobile version