Skip to main content

C++ Variadic Templates

Introduction

Variadic templates are a powerful feature introduced in C++11 that allows functions and classes to accept an arbitrary number of arguments with different types. Before variadic templates, C++ developers had to create multiple overloaded functions or use workarounds to handle varying numbers of arguments. Variadic templates provide a type-safe, elegant solution to this problem, enabling you to write more flexible and reusable code.

In this tutorial, we'll explore how variadic templates work, how to implement them, and their practical applications in modern C++ programming.

Understanding Variadic Templates

A variadic template is a template that can accept a variable number of arguments. This is achieved using the ellipsis (...) syntax in two contexts:

  1. Parameter pack: A template parameter that accepts zero or more arguments
  2. Pack expansion: The process of unpacking these parameters in your code

Let's start with a simple example:

cpp
#include <iostream>

// Base case: when no arguments are left
void print() {
std::cout << std::endl;
}

// Variadic template function
template<typename T, typename... Args>
void print(T first, Args... args) {
std::cout << first << " ";
print(args...); // Recursive call with remaining arguments
}

int main() {
print(1, 2.5, "Hello", 'c', true);
return 0;
}

Output:

1 2.5 Hello c 1

Breaking Down the Code

In the example above:

  • typename... Args declares a template parameter pack that can accept any number of arguments of any type.
  • Args... args defines a function parameter pack that holds the actual values.
  • print(args...) is a pack expansion that unpacks the arguments for the recursive call.
  • We define a base case print() with no arguments to terminate the recursion.

How Parameter Packs Work

A parameter pack is a template parameter that accepts zero or more arguments. There are a few important concepts to understand about parameter packs:

  1. Declaring a parameter pack: Use the ellipsis (...) after the parameter type
  2. Accessing the number of arguments: Use the sizeof...(pack_name) operator
  3. Expanding a parameter pack: Use the pack name followed by ...

Let's see these concepts in action:

cpp
#include <iostream>

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

int main() {
showPackSize(); // 0 arguments
showPackSize(1); // 1 argument
showPackSize(1, 2.5, 'a'); // 3 arguments
return 0;
}

Output:

Number of arguments: 0
Number of arguments: 1
Number of arguments: 3

Parameter Pack Expansion Techniques

There are several ways to expand parameter packs:

1. Recursive Expansion (as shown earlier)

cpp
#include <iostream>

// Base case
void print() {
std::cout << std::endl;
}

// Recursive case
template<typename T, typename... Args>
void print(T first, Args... args) {
std::cout << first << " ";
print(args...); // Recursive call
}

2. Fold Expressions (C++17)

Fold expressions provide a more concise way to operate on all elements of a parameter pack:

cpp
#include <iostream>

// Using fold expressions (C++17)
template<typename... Args>
void print(Args... args) {
((std::cout << args << " "), ...);
std::cout << std::endl;
}

int main() {
print(1, 2.5, "Hello", 'c', true);
return 0;
}

Output:

1 2.5 Hello c 1

3. Using Initializer Lists

Another technique is to use initializer lists to expand parameter packs:

cpp
#include <iostream>

template<typename... Args>
void print(Args... args) {
// Using an initializer list and a lambda to handle each argument
(void)std::initializer_list<int>{
(std::cout << args << " ", 0)...
};
std::cout << std::endl;
}

Practical Applications of Variadic Templates

1. Implementing a Type-Safe printf

One classic application is creating a type-safe version of printf:

cpp
#include <iostream>
#include <string>

// Base case for recursion
void safe_printf(const std::string& format) {
std::cout << format;
}

template<typename T, typename... Args>
void safe_printf(const std::string& format, T value, Args... args) {
size_t pos = format.find("{}");
if (pos != std::string::npos) {
std::cout << format.substr(0, pos) << value;
safe_printf(format.substr(pos + 2), args...);
} else {
std::cout << format;
}
}

int main() {
safe_printf("Hello, {}! You have {} new messages.\n", "John", 5);
return 0;
}

Output:

Hello, John! You have 5 new messages.

2. Generic Tuple Implementation

Variadic templates are ideal for implementing container classes like std::tuple:

cpp
#include <iostream>

// Simple tuple implementation using variadic templates
template<typename... Types>
class Tuple;

// Base case: empty tuple
template<>
class Tuple<> {
public:
static constexpr std::size_t size = 0;
};

// Recursive case
template<typename Head, typename... Tail>
class Tuple<Head, Tail...> {
private:
Head head;
Tuple<Tail...> tail;

public:
static constexpr std::size_t size = sizeof...(Tail) + 1;

Tuple(Head h, Tail... t) : head(h), tail(t...) {}

Head getHead() const { return head; }
const Tuple<Tail...>& getTail() const { return tail; }
};

// Helper function to get element at index I
template<std::size_t I, typename Head, typename... Tail>
struct GetHelper;

template<typename Head, typename... Tail>
struct GetHelper<0, Head, Tail...> {
static Head get(const Tuple<Head, Tail...>& tuple) {
return tuple.getHead();
}
};

template<std::size_t I, typename Head, typename... Tail>
struct GetHelper {
static auto get(const Tuple<Head, Tail...>& tuple) {
return GetHelper<I-1, Tail...>::get(tuple.getTail());
}
};

template<std::size_t I, typename... Types>
auto get(const Tuple<Types...>& tuple) {
return GetHelper<I, Types...>::get(tuple);
}

int main() {
Tuple<int, double, std::string> tuple(1, 2.5, "Hello");

std::cout << "First element: " << get<0>(tuple) << std::endl;
std::cout << "Second element: " << get<1>(tuple) << std::endl;
std::cout << "Third element: " << get<2>(tuple) << std::endl;

return 0;
}

Output:

First element: 1
Second element: 2.5
Third element: Hello

3. Perfect Forwarding with Variadic Templates

Variadic templates work perfectly with perfect forwarding to create highly generic factory functions:

cpp
#include <iostream>
#include <memory>
#include <string>

class Person {
private:
std::string name;
int age;

public:
Person(std::string n, int a) : name(std::move(n)), age(a) {
std::cout << "Person constructed with name=" << name << ", age=" << age << std::endl;
}

void introduce() const {
std::cout << "Hi, I'm " << name << " and I'm " << age << " years old." << std::endl;
}
};

// Factory function using perfect forwarding
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 person = make_unique<Person>("Alice", 30);
person->introduce();

return 0;
}

Output:

Person constructed with name=Alice, age=30
Hi, I'm Alice and I'm 30 years old.

Variadic Class Templates

Just like functions, classes can also be variadic:

cpp
#include <iostream>
#include <string>

// Variadic class template
template<typename... Types>
class TypeList {
public:
static constexpr std::size_t size = sizeof...(Types);

template<typename... Args>
static void printTypeInfo(Args&&... args) {
std::cout << "TypeList contains " << size << " types" << std::endl;
printArgs(std::forward<Args>(args)...);
}

private:
template<typename T>
static void printArgs(T&& arg) {
std::cout << "- " << arg << std::endl;
}

template<typename T, typename... Rest>
static void printArgs(T&& arg, Rest&&... rest) {
std::cout << "- " << arg << std::endl;
printArgs(std::forward<Rest>(rest)...);
}
};

int main() {
TypeList<int, double, std::string> list;
list.printTypeInfo("Integer", "Double", "String");

return 0;
}

Output:

TypeList contains 3 types
- Integer
- Double
- String

Advanced: Variadic Template Metaprogramming

Variadic templates can be used for compile-time metaprogramming:

cpp
#include <iostream>
#include <type_traits>

// Check if a type exists in a type list
template<typename T, typename... Types>
struct Contains;

template<typename T>
struct Contains<T> {
static constexpr bool value = false;
};

template<typename T, typename U, typename... Types>
struct Contains<T, U, Types...> {
static constexpr bool value =
std::is_same<T, U>::value || Contains<T, Types...>::value;
};

// Type that holds a compile-time integer sequence
template<int... Ns>
struct Sequence {};

// Generate a sequence of integers from 0 to N-1
template<int N, int... Ns>
struct MakeSequence : MakeSequence<N-1, N-1, Ns...> {};

template<int... Ns>
struct MakeSequence<0, Ns...> {
using type = Sequence<Ns...>;
};

int main() {
constexpr bool hasInt = Contains<int, double, float, int, char>::value;
constexpr bool hasString = Contains<std::string, double, float, int, char>::value;

std::cout << "Type list contains int: " << std::boolalpha << hasInt << std::endl;
std::cout << "Type list contains string: " << hasString << std::endl;

// Using the integer sequence
using Seq = typename MakeSequence<5>::type; // Sequence<0,1,2,3,4>

return 0;
}

Output:

Type list contains int: true
Type list contains string: false

Performance Considerations

Variadic templates are resolved at compile time, so they have no runtime overhead compared to manually writing out all the individual function overloads. However, recursive template instantiation can increase compile time and code size. For very large parameter packs, this can lead to:

  1. Longer compilation times
  2. Potentially larger binaries due to template instantiation

For most applications, these concerns are minor compared to the benefits of code reusability and type safety.

Common Pitfalls and Solutions

1. Missing Base Case

One common mistake is forgetting to define a base case for recursion:

cpp
// Error: Missing base case
template<typename T, typename... Args>
void print(T first, Args... args) {
std::cout << first << " ";
print(args...); // Will fail when args is empty!
}

Solution: Always provide a base case:

cpp
// Base case
void print() {
std::cout << std::endl;
}

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

2. Order of Overload Resolution

Sometimes the compiler may not choose the overload you expect:

cpp
void process(int n) {
std::cout << "Non-template version: " << n << std::endl;
}

template<typename... Args>
void process(Args... args) {
std::cout << "Variadic template version with "
<< sizeof...(args) << " arguments" << std::endl;
}

// Calling process(5) may be ambiguous or call the wrong version

Solution: Be explicit about which overload should be preferred by using SFINAE or if constexpr.

Summary

Variadic templates are a powerful C++11 feature that allows you to create functions and classes that can work with an arbitrary number of arguments of different types. Key points to remember:

  • Use the ellipsis (...) syntax to declare parameter packs
  • Expand parameter packs using pack expansion or fold expressions
  • Always provide a base case for recursive variadic templates
  • Use sizeof...(args) to get the number of arguments in a pack
  • Variadic templates are resolved at compile time, so they're type-safe and have no runtime overhead

With variadic templates, you can create more flexible, reusable code that works with any number of arguments while maintaining type safety – a significant improvement over the C-style variadic functions like printf.

Exercises

  1. Implement a variadic function sum that calculates the sum of all its arguments.
  2. Create a variadic template class Stack that can store elements of different types.
  3. Implement a format function similar to Python's string formatting, using variadic templates.
  4. Create a function that checks if an element exists in a variadic list of arguments.
  5. Implement a simple event system using variadic templates for the event handlers.

Additional Resources



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)