Skip to content

Instantly share code, notes, and snippets.

@MangaD
Last active July 6, 2025 13:26
Show Gist options
  • Save MangaD/f7078f511b400b63348dbc975df959a2 to your computer and use it in GitHub Desktop.
Save MangaD/f7078f511b400b63348dbc975df959a2 to your computer and use it in GitHub Desktop.
C++: Variadic Templates

C++: Variadic Templates

CC0

Disclaimer: Grok generated document.

Variadic templates in C++ are a powerful feature introduced in C++11 that allow functions and classes to accept a variable number of arguments of different types. They enable flexible, type-safe, and generic programming, often used in libraries, metaprogramming, and scenarios requiring dynamic argument handling. Below, I’ll provide a comprehensive overview of variadic templates, their syntax, use cases, and related concepts, keeping the explanation concise yet detailed.


1. What Are Variadic Templates?

Variadic templates allow you to define functions or class templates that can take an arbitrary number of arguments (zero or more) of any type. They are primarily used to:

  • Write generic code that handles multiple arguments flexibly.
  • Avoid repetitive code for functions or classes with varying numbers of parameters.
  • Enable compile-time recursion for processing argument lists.

The key syntax involves the ... (ellipsis) operator, which indicates a parameter pack, and typename... or class... to declare a pack of types.


2. Syntax and Core Concepts

Parameter Packs

A parameter pack is a template parameter or function parameter that represents a variable number of arguments. There are two types:

  • Template Parameter Pack: Declared in a template definition, e.g., typename... Args.
  • Function Parameter Pack: Declared in a function parameter list, e.g., Args... args.

Example:

template<typename... Args> // Template parameter pack
void print(Args... args); // Function parameter pack

Pack Expansion

The ... operator is used to expand a parameter pack into individual elements. For example, in a function call, args... expands to a comma-separated list of arguments.

Basic Example

#include <iostream>

template<typename... Args>
void print(Args... args) {
    // Expands to print each argument
    ((std::cout << args << " "), ...); // Fold expression (C++17)
}

int main() {
    print(1, "hello", 3.14); // Outputs: 1 hello 3.14
    return 0;
}

Here, Args is a template parameter pack, and args is a function parameter pack. The fold expression ((std::cout << args << " "), ...) expands to print each argument with a space.


3. Key Features and Mechanisms

Recursion for Processing Parameter Packs

Before C++17, variadic templates often used recursion to process arguments, as there was no direct way to iterate over a pack. A base case (non-variadic overload) is typically defined to terminate recursion.

Example:

#include <iostream>

// Base case to terminate recursion
void print() {}

// Recursive case
template<typename T, typename... Args>
void print(T first, Args... rest) {
    std::cout << first << " ";
    print(rest...); // Recursively process the rest
}

int main() {
    print(1, "hello", 3.14); // Outputs: 1 hello 3.14
    return 0;
}

Fold Expressions (C++17)

C++17 introduced fold expressions, which simplify processing parameter packs by eliminating the need for explicit recursion. Fold expressions apply an operator (e.g., +, <<, ,) to all elements in a pack.

There are two types of fold expressions:

  • Unary Fold: Applies an operator to all elements in a pack.
  • Binary Fold: Combines a pack with an initial value using an operator.

Syntax:

  • Unary right fold: (pack op ...) or (... op pack)
  • Binary fold: (pack op ... op init) or (init op ... op pack)

Example:

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

int main() {
    std::cout << sum(1, 2, 3, 4) << std::endl; // Outputs: 10
    return 0;
}

sizeof... Operator

The sizeof... operator returns the number of elements in a parameter pack at compile time.

Example:

template<typename... Args>
void count_args(Args... args) {
    std::cout << "Number of arguments: " << sizeof...(args) << std::endl;
}

int main() {
    count_args(1, "hello", 3.14); // Outputs: Number of arguments: 3
    return 0;
}

4. Use Cases

1. Function Argument Forwarding

Variadic templates are commonly used for perfect forwarding in functions like std::make_unique or std::make_shared.

Example:

#include <memory>
#include <string>

template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args) {
    return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}

int main() {
    auto ptr = make_unique<std::string>("Hello", 5); // Constructs std::string("Hello")
    return 0;
}

Here, std::forward preserves the value category (lvalue/rvalue) of the arguments.

2. Tuple Implementation

Variadic templates are the foundation for std::tuple, which stores a heterogeneous collection of elements.

Example:

#include <tuple>

std::tuple<int, std::string, double> t(1, "hello", 3.14);
// Access elements with std::get

Internally, std::tuple uses variadic templates to define a recursive structure.

3. Type-Safe printf

Variadic templates can create type-safe alternatives to C-style functions like printf.

Example:

#include <iostream>

template<typename... Args>
void safe_printf(const char* format, Args... args) {
    // Simplified: assumes format matches args
    ((std::cout << args << " "), ...);
}

int main() {
    safe_printf("%s %d %f", "test", 42, 3.14); // Outputs: test 42 3.14
    return 0;
}

4. Variadic Class Templates

Variadic templates can also be used with classes, such as for type lists or custom data structures.

Example:

template<typename... Types>
struct TypeList {};

TypeList<int, double, std::string> my_list; // A type list with three types

5. Advanced Topics

Template Parameter Pack Constraints

You can constrain parameter packs using concepts (C++20) or SFINAE (pre-C++20) to restrict the types accepted.

Example with Concepts:

#include <concepts>
#include <iostream>

template<typename... Args>
    requires (std::integral<Args> && ...)
void print_integers(Args... args) {
    ((std::cout << args << " "), ...);
}

int main() {
    print_integers(1, 2, 3); // Works
    // print_integers(1, "hello", 3); // Fails: "hello" is not integral
    return 0;
}

Non-Type Template Parameter Packs

Variadic templates can also handle non-type parameters (e.g., integers, pointers).

Example:

template<int... Values>
int sum_values() {
    return (Values + ...);
}

int main() {
    std::cout << sum_values<1, 2, 3, 4>() << std::endl; // Outputs: 10
    return 0;
}

Variadic Template Inheritance

Variadic templates can be used to implement recursive inheritance patterns, such as the curiously recurring template pattern (CRTP) or tuple-like structures.

Example:

template<typename... Types>
struct Tuple {};

template<typename Head, typename... Tail>
struct Tuple<Head, Tail...> : Tuple<Tail...> {
    Head value;
};

Compile-Time Computations

Variadic templates are often used in template metaprogramming to perform computations at compile time, such as computing factorials or generating type lists.


6. Related Concepts

std::tuple and std::variant

  • std::tuple uses variadic templates to store a fixed-size collection of heterogeneous types.
  • std::variant (C++17) complements variadic templates by allowing a type-safe union of a fixed set of types.

Perfect Forwarding

Variadic templates are often paired with std::forward and universal references (T&&) to preserve argument types and value categories.

Template Metaprogramming

Variadic templates enhance template metaprogramming by enabling recursive type manipulation, such as type traits or compile-time algorithms.

Fold Expressions vs. Recursion

Fold expressions (C++17) reduce the need for recursive function definitions, making code more concise and readable.


7. Limitations and Considerations

  1. Compile-Time Overhead: Variadic templates can increase compilation time, especially with deep recursion or large parameter packs.
  2. Complexity: Recursive implementations or complex pack expansions can be hard to debug or maintain.
  3. Error Messages: Template-related errors can be verbose and difficult to interpret.
  4. Pre-C++17: Without fold expressions, recursion is required, which can make code verbose.

8. Best Practices

  • Use fold expressions (C++17+) for simpler, more readable code.
  • Leverage concepts (C++20) to constrain parameter packs for better type safety.
  • Use sizeof... to handle edge cases (e.g., empty packs).
  • Combine with std::forward for perfect forwarding in generic code.
  • Test with a variety of argument types and counts to ensure robustness.

9. Practical Example: Logging Function

Here’s a practical example combining several concepts:

#include <iostream>
#include <string>

// Base case for empty pack
void log() { std::cout << std::endl; }

template<typename T, typename... Args>
void log(T first, Args... rest) {
    std::cout << first << " ";
    log(rest...);
}

template<typename... Args>
void debug_log(const char* prefix, Args... args) {
    std::cout << prefix << " (" << sizeof...(args) << " args): ";
    log(args...);
}

int main() {
    debug_log("DEBUG", 1, "test", 3.14); // Outputs: DEBUG (3 args): 1 test 3.14
    return 0;
}

10. Resources for Further Learning

  • C++ Reference: cppreference.com for detailed variadic template documentation.
  • Books:
    • C++ Templates: The Complete Guide by David Vandevoorde, Nicolai M. Josuttis, and Douglas Gregor.
    • Modern C++ Design by Andrei Alexandrescu for advanced template techniques.
  • Standards: Check C++11, C++17, and C++20 standards for variadic templates, fold expressions, and concepts.

If you have specific questions about variadic templates, want a deeper dive into a particular aspect, or need help with a code example, let me know!


Explaining ((std::cout << args << " "), ...)

CC0

Disclaimer: ChatGPT generated document.

Let's break down the fold expression:

((std::cout << args << " "), ...);

This is a fold expression, introduced in C++17, used to "fold" a parameter pack (Args... args) over a binary operator β€” in this case, the comma operator ,.


βœ… What is a fold expression?

A fold expression allows you to apply an operator to a pack of arguments in a clean, concise way. It saves you from having to use recursion or initializer lists to expand parameter packs.

In this case:

  • args... is a pack of arguments (in your example: 1, "hello", 3.14)
  • std::cout << args << " " is applied to each argument
  • The fold over the comma operator , ensures the expressions execute in left-to-right order

πŸ“¦ What this expands to

This line:

((std::cout << args << " "), ...);

Is a unary left fold over the comma operator. It roughly expands to:

((std::cout << 1 << " "), (std::cout << "hello" << " "), (std::cout << 3.14 << " "));

Each individual expression inside the fold:

std::cout << args << " "

prints the current argument followed by a space.

The comma operator , simply evaluates all these expressions in order. It throws away the result of each expression but ensures side effects (like printing) happen in order.


🧠 Why use the comma operator here?

The comma operator is used to sequence expressions. Because operator<< returns std::ostream&, we could also try to fold using <<, but that would require more careful chaining (e.g., ((std::cout << args) << ...)), which has complications if args are of different types or involve expressions with side effects.

Using the comma fold gives a clean way to loop over the pack and execute side effects like printing, one by one.


πŸ” Alternative (pre-C++17)

Before C++17, you'd do something like:

template<typename... Args>
void print(Args... args) {
    (void)std::initializer_list<int>{(std::cout << args << " ", 0)...};
}

This uses a trick with initializer_list and comma operator to expand the pack.


βœ… Summary

((std::cout << args << " "), ...);
  • Is a C++17 fold expression
  • Applies std::cout << args << " " to each args...
  • Uses the comma operator to sequence the prints
  • Expands to std::cout << arg1 << " "; std::cout << arg2 << " "; ...

It’s a neat, readable way to print variadic arguments, one by one, in order.

Let me know if you want visualizations, alternatives, or how to make this work with custom formatting!


Sure! Here's a complete and expanded explanation of the ellipsis ... in C++, now including:

  • Why the ... works after (std::cout << args << " ", 0) (not a fold expression),
  • A breakdown of C++20's parameter pack expansion with foreach-like constructs.

βœ… Is there an ellipsis operator in C++?

CC0

Disclaimer: ChatGPT generated document.

Yes β€” the ellipsis ... in C++ is a syntactic construct, not a normal operator like +. It's used for variadic function arguments, template parameter packs, and pack expansion. It behaves differently depending on where and how it's used.


πŸ”Ή 1. C-Style Variadic Functions (Pre-C++11)

The oldest use of ... is in C-style variadic functions, introduced in C.

πŸ“˜ Example:

#include <cstdarg>
#include <iostream>

void print(const char* fmt, ...) {
    va_list args;
    va_start(args, fmt);

    for (const char* p = fmt; *p != '\0'; ++p) {
        if (*p == 'd') {
            int i = va_arg(args, int);
            std::cout << i << " ";
        } else if (*p == 's') {
            const char* s = va_arg(args, const char*);
            std::cout << s << " ";
        }
    }

    va_end(args);
    std::cout << "\n";
}

int main() {
    print("ds", 42, "hello");
}

⚠️ Limitations:

  • No type checking β€” the compiler can't verify the types of the passed arguments.
  • Relies on convention (fmt string) to know what's coming.
  • Easy to misuse.

πŸ”Ή 2. Variadic Templates (C++11 and beyond)

C++11 introduced variadic templates, enabling type-safe variable argument lists.

template <typename... Args>
void print(Args... args);
  • Args... is a parameter pack of types.
  • args... is the corresponding pack of function arguments.

But how do you use those arguments?


πŸ”Ή 3. Parameter Pack Expansion with Ellipsis

To do something with each argument, you use the ellipsis ... to expand the pack.

βœ… This is valid:

template <typename... Args>
void print(Args... args) {
    int dummy[] = { (std::cout << args << " ", 0)... };
    std::cout << "\n";
}

🧠 Why does this work?

Let's break it down:

int dummy[] = { (std::cout << args << " ", 0)... };
  • args... expands to each function argument.

  • (std::cout << args << " ", 0) is a comma expression:

    • Evaluates std::cout << args << " ",
    • Then evaluates 0 (which is needed to create a valid array element).
  • The ellipsis ... follows the expression, meaning:

    • It expands the entire expression for each argument in args.

Example:

If args... is 1, "hello", 3.14, then:

int dummy[] = {
  (std::cout << 1 << " ", 0),
  (std::cout << "hello" << " ", 0),
  (std::cout << 3.14 << " ", 0)
};

This ensures that each args is printed, and all expressions evaluate to 0, making a valid int[].

πŸ’‘ Important: This is not a fold expression! Fold expressions (C++17) are more elegant, but this initializer list trick works in C++11 and C++14.


πŸ”Ή 4. Fold Expressions (C++17)

C++17 introduced a much cleaner syntax:

template<typename... Args>
void print(Args... args) {
    ((std::cout << args << " "), ...);
    std::cout << "\n";
}

This is a left fold over the << operator:

  • Equivalent to:

    (((std::cout << arg1) << arg2) << arg3)...;

More generally, a fold expression looks like:

(... op args)       // right fold
((args op) ...)     // left fold

βœ… Advantage: Cleaner syntax, no dummy array or comma expression required.


πŸ”Ή 5. C++20 "foreach" Style for Parameter Packs

C++20 allows you to use lambdas with pack expansion in a way that feels like foreach.

βœ… Example:

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

This is a fold expression with the comma operator:

  • Evaluates (std::cout << args << " ") for each argument,
  • The comma operator guarantees left-to-right evaluation.

Or using a lambda:

template <typename... Args>
void print(Args&&... args) {
    auto foreach = [](auto&& arg) {
        std::cout << arg << " ";
    };
    (foreach(std::forward<Args>(args)), ...);
}

This lets you simulate a foreach over a parameter pack.


πŸ”Ή 6. Summary Table: Ways to Iterate over Packs

Method C++ Version Notes
int dummy[] = { (f(args), 0)... }; C++11 Tricky but effective; ensures order
((std::cout << args), ...) C++17 Fold expression
(lambda(args), ...) C++17/20 Readable with lambdas
(..., f(args)) C++17 Right fold with comma, left-to-right

🧠 Final Takeaways

  • The ellipsis ... expands a parameter pack, and how you use it depends on the surrounding syntax.
  • In C++11/C++14, we rely on initializer lists and comma expressions to safely iterate packs.
  • In C++17, fold expressions give us a native, readable syntax.
  • In C++20, pack-expanding lambdas make code even clearer.

Would you like a comparison of recursive unpacking vs fold expressions?


Where is pack expansion allowed?

CC0

Disclaimer: ChatGPT generated document.

Great question! You're diving into the mechanics of pack expansion in C++, and it's crucial to understand why some syntax is legal and some isn't, even when it looks intuitive.

Let’s walk through both versions of the print function and explain why the first one compiles and works, and why the second one doesn't.


βœ… Working Version

template <typename... Args>
void print(Args... args) {
    int dummy[] = { (std::cout << args << " ", 0)... };
    std::cout << "\n";
}

πŸ” Why this works:

This code uses C++11 pack expansion syntax inside an initializer list.

πŸ”‘ The critical parts:

  • (std::cout << args << " ", 0) is a comma expression:

    • Evaluates std::cout << args << " " for side effects (printing).
    • Then yields 0 so the expression is a valid int (needed for the array).
  • { ... } is an initializer list, and pack expansion is allowed here.

  • dummy[] = { ... } ensures that:

    • The code executes in left-to-right order (guaranteed by initializer lists).
    • All arguments are printed.
    • dummy is unused (only created to force evaluation of each expression).

πŸ” Expansion:

If you call print(1, "hello", 3.14), the expansion becomes:

int dummy[] = {
  (std::cout << 1 << " ", 0),
  (std::cout << "hello" << " ", 0),
  (std::cout << 3.14 << " ", 0)
};

❌ Broken Version

template<typename... Args>
void print(Args... args) {
    (std::cout << args << " ")...; // ❌ Invalid
}

❌ Why this does not work:

This line attempts to use pack expansion directly:

(std::cout << args << " ")...;

But C++ does not allow a ... to be applied to arbitrary expressions like this β€” outside of a context that supports pack expansion (e.g., initializer list, fold expression, function call, etc.).

❗Problem:

  • You’re writing an expression list β€” expr...; β€” outside any structure that defines how the pack should be expanded.
  • The compiler doesn’t know what to do with the ellipsis here.
  • There is no valid C++ grammar that allows an expression like this to expand a pack outside of a fold, initializer list, function call, etc.

βœ… Valid Alternatives (C++17 and C++20)

βœ… C++17 Fold Expression:

template<typename... Args>
void print(Args... args) {
    ((std::cout << args << " "), ...); // Fold expression (left fold)
    std::cout << "\n";
}

This is a fold expression, introduced in C++17:

  • Left fold over << and the args pack.

  • Equivalent to:

    (((std::cout << arg1) << arg2) << arg3)...;

βœ… C++20 β€œforeach” with lambda:

template <typename... Args>
void print(Args&&... args) {
    auto foreach = [](auto&& arg) {
        std::cout << arg << " ";
    };
    (foreach(std::forward<Args>(args)), ...); // Right fold using comma
    std::cout << "\n";
}

This is a fold expression using the comma operator, which is also allowed.


🧠 Summary

Code Valid? Why / Why Not
int dummy[] = { (std::cout << args << " ", 0)... }; βœ… C++11-compliant, expands inside initializer list
(std::cout << args << " ")...; ❌ Invalid: pack expansion not allowed in a raw expression list
((std::cout << args << " "), ...); βœ… C++17 fold expression syntax
(lambda(args), ...); βœ… C++17+ valid fold expression with comma
std::apply([](auto&&... xs) { (std::cout << xs << " ", ...); }, t) βœ… C++17 std::apply + lambda with fold
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment