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.