In typed languages like C++, you are always writing codes with fixed types like int add(int x, int y). This means that if you want to write the equivalent logic for a different data type (for example a float) you have to repeat the code gain.

Templates solve that problem in C++ by letting you write generic code. That sounds nice but this is C++: if its going to do something, its going to do it to an excessive degree.

That’s where variadic templates come in. “Variadic” here means that your generic code can accept an arbitrary number of types. A good example is to consider the difference between std::pair and std::tuple.

// pair expects exactly two types
std::pair<bool, int> a;

// tuples can accept arbitrary number of types
std::tuple<bool, int, float> b;
std::tuple<int> weird;
std::tuple<> still_valid;

Since std::tuple supports an arbitrary number of types, they must be implemented in a way that can support an arbitrary number of types.

Motivation

I was playing around with the sqlite library in this project and I wanted to implement a function that can generically support fetching arbitrary number of columns from the database as part of a SELECT SQL query.

So what would that interface look like?

template<typename... Ts> // Ts represents a group of types
std::vector<std::tuple<Ts...>> get_data(const std::string& sql);

Here we declare a function that is a template. The template accepts an arbitrary number of arguments and we use that to set the return type. Here we define a vector of tuples where each tuple represents a row of data. Each column in the row is an element in the tuple.

Now conceptually, when we write this code, we want to be able to iterate on each of the types in the “parameter list”. This way we can pull the field from the database for each column type. So how can we iteratre on the types? Let’s look at an example on cppreference:

// Adapted from cppreference
void tprintf() // base function
{
    std::cout << "Done";
}
 
template<typename T, typename... Targs>
void tprintf(T value, Targs... extras) // recursive function
{
    std::cout << value;
    tprintf(extras...); // recursive call
    return;
}

// Example call
tprintf(123, "TEST", 3.14);

This code is very similar to how lists are handled in functional languages. You take the first element of the list, handle it, and put the rest of the elements in the “tail”. Then you can recurse on the “tail” as your new list. With each function call, you make your way through the list.

That’s what the code is doing. Here are what each of the “compile time iterations” look like:

tprintf(value=123, extras=["TEST", 3.14])
tprintf(value="TEST", extras=[3.14])
tprintf(value=3.14, extras=[])
// magic happens here
tprintf() // This calls the base case, not the template

The reason the base case works is that normal C++ rules for function overloading kick in. The recursive function accepts at least one argument and the base function accepts 0 arguments. So when we get to the “end of the list”, we call the non-templated base function because it better matches the argument we are providing.

Variadic Templates Without Function Overloading

Unfortunately this trick doesn’t work for us. Our “variadic” behavior is in the return type. Return types don’t “participate” in the C++ overloading rules. So this wouldn’t work:

tuple<> get_data() // base function
{
    return {};
}

template<typename T, typename...Targs>
tuple<T, Targs...> get_data()
{
    return tuple_cat(make_tuple(T()), get_data());
}

This fails because after the first invocation get_data will immediately call the nontemplatized get_data. This ends the recursion early because the compiler doesn’t know that it should continue calling the templatized function.

To fix this, we have to tell the compiler to explicitly call the template function by calling get_data<Targs...>(). However, then the compiler never calls the base function because its always looking to call a template function. So in the final base case the compiler is looking to call a template function get_data<>() with no template arguments.

And now we are stuck because while the compiler is looking for a function with no template parameters, there is no way for us to actually definte a template function with no template parameters.

Hack: Add an arbitrary parameter

One hack we can do is to add a dummy template argument so that our base case can accept one dummy template parameter. Since our problem is that we can’t define get_data<>(), then we can instead define get_data<DUMMY>() as our base case.

Here our dummy is int N which the caller can set to any integer.

template <int N>
tuple<> get_data() // base function
{
    return {};
}

template<int N, typename T, typename...Targs>
tuple<T, Targs...> get_data()
{
    return tuple_cat(make_tuple(T()), get_data<N, Targs...>());
}

Compile-time Conditional

A less ugly way is to abandon our recursion and do a compile-time check to see how many args are remaining in Targs.

template<typename T, typename...Targs>
tuple<T, Targs...> get_data()
{
  if constexpr (sizeof...(Targs) == 0) {
    return make_tuple(T());
  }
  else {
    return tuple_cat(make_tuple(T()), get_data<Targs...>());
  }
}

Variadic Expansion

Variadic templates can also be expanded using ... so we don’t have to do the head/tail trick at all. If we can solve our problem by repeating the sam expression for each variadic argument, then we can use variadic expansion to solve our problem.

template<typename...Targs>
tuple<Targs...> get_data()
{
  return tuple_cat(make_tuple(Targs())...);
}

If this template is invoked by calling get_data<int, bool>() then the generated code might look something like this:

return tuple_cat(make_tuple(int()), make_tuple(bool()));

This code can actually be further simplified since we are creating all the elements inline, we don’t need tuple_cat anymore.

template<typename...Targs>
tuple<Targs...> get_data()
{
  return {Targs()...};
  // would generate return {int(), bool()};
}

In essence, the variadic expension separates all the expressions with a , and then regular C++ rules kick in. If we need the expressions to join using someo ther operator (like +) then folding expressions can be used as well.