C++ Insights - Variadic Templates
This is a cross-post from www.ModernesCpp.com.
Variadic templates are a C++ feature which looks quite magic the first time you see it. Thanks to C++ Insights, most of the magic disappears.
Variadic templates are one of the powerful new constructs we have since C++11.
Variadic Templates
They are great because we can have a function that takes multiple arguments and still is strongly typed. We do not need a format specifier to cast some memory from the stack into a type. Variadic templates or in this case more precisely variadic function templates, expand into functions as we would write them. The so-called parameter pack gets expanded. During this process, each parameter is simply separated by a comma, just like we would write the function. Here is a basic example:
template<typename T>
T add(const T& arg)
{
return arg;
}
template<typename T, typename... ARGS>
T add(const T& arg, const ARGS&... args)
{
return arg + add(args...);
}
int main()
{
return add(1, 2u, 3u);
}
The single argument overload is required to terminate the recursion I used here. Let’s use C++ Insights to see what’s going on under the hood:
template<typename T>
T add(const T& arg)
{
return arg;
}
/* First instantiated from: insights.cpp:10 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
unsigned int add<unsigned int>(const unsigned int & arg)
{
return arg;
}
#endif
template<typename T, typename... ARGS>
T add(const T& arg, const ARGS&... args)
{
return arg + add(args...);
}
/* First instantiated from: insights.cpp:15 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
int add<int, unsigned int, unsigned int>(const int & arg, const unsigned int & __args1, const unsigned int & __args2)
{
return static_cast<int>(static_cast<unsigned int>(arg) + add(__args1, __args2));
}
#endif
/* First instantiated from: insights.cpp:10 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
unsigned int add<unsigned int, unsigned int>(const unsigned int & arg, const unsigned int & __args1)
{
return arg + add(__args1);
}
#endif
int main()
{
return add(1, 2u, 3u);
}
Pay attention to the types. I used 2u and 3u which results in two unsigned int arguments and one signed int. Due to the arrangement of the parameters the return type of add is int which leads, as C++ Insights shows use, to an implicit cast in add. One additional insight C++ Insights shows us.
Fold Expressions
With C++17 and fold-expressions we can reduce our code to this:
template<typename... ARGS>
auto add(const ARGS&... args)
{
return (args + ...);
}
int main()
{
return add(1, 2u, 3u);
}
I really like how the new standards make us write less and less code. The result of C++ Insights changes as well:
template<typename... ARGS>
auto add(const ARGS&... args)
{
return (args + ...);
}
/* First instantiated from: insights.cpp:9 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
unsigned int add<int, unsigned int, unsigned int>(const int & __args0, const unsigned int & __args1, const unsigned int & __args2)
{
return static_cast<unsigned int>(__args0) + __args1 + __args2;
}
#endif
int main()
{
return static_cast<int>(add(1, 2u, 3u));
}
Of course, you can write variadic class templates as well. Have a look at this code sample:
template<int...>
struct add;
template<>
struct add<>
{
static constexpr int value = 0;
};
template<int i, int... tail>
struct add<i, tail...>
{
static constexpr int value = i + add<tail...>::value;
};
static_assert(6 == add<1, 2, 3>::value, "Expect 6");
We have a variadic class template which calculates the sum of an arbitrary amount of numbers. C++ Insights shows all the instantiations that happen in the background to calculate the result. Here we can see how it pops one number each time until there are no more left:
template<int...>
struct add;
/* First instantiated from: insights.cpp:16 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
struct add<1, 2, 3>
{
inline static constexpr const int value = 1 + add<2, 3>::value;
};
#endif
/* First instantiated from: insights.cpp:13 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
struct add<2, 3>
{
inline static constexpr const int value = 2 + add<3>::value;
};
#endif
/* First instantiated from: insights.cpp:13 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
struct add<3>
{
inline static constexpr const int value = 3 + add<>::value;
};
#endif
template<>
struct add<>
{
static constexpr int value = 0;
};
template<int i, int... tail>
struct add<i, tail...>
{
static constexpr int value = i + add<tail...>::value;
};
/* PASSED: static_assert(6 == add<1, 2, 3>::value, "Expect 6"); */
Reference to an Array
One more thing about templates is that they can take a reference to an array which prevents an array from decaying to a pointer:
template<typename T, int N>
void Rx(T (&data)[N])
{
// assuming char here
static_assert(sizeof(data) == 5);
}
int main()
{
char buffer[5]{};
Rx(buffer);
}
In C++ Insights you can see that the instantiation contains the type as well as the size of the array:
template<typename T, int N>
void Rx(T (&data)[N])
{
// assuming char here
static_assert(sizeof(data) == 5);
}
/* First instantiated from: insights.cpp:12 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
void Rx<char, 5>(char (&data)[5])
{
/* PASSED: static_assert(sizeof(data) == 5); */
}
#endif
int main()
{
char buffer[5] = {'\0', '\0', '\0', '\0', '\0'};
Rx(buffer);
}
Another thing, aside from templates, you can see in this example the effect of braced initialization of buffer. The compiler fills all elements of the array for us with the default value. This means that we can say goodbye to the good old memset and make our programs faster and safer.
I hope I could show you how C++ Insights can be helpful if it comes to templates. For me, it is a vital instrument when teaching and explaining templates especially variadic templates. Try it out and tell me about your experience.
I’d like to thank Rainer for the opportunity to share information about C++ Insights on his popular blog!
Have fun with C++ Insights. You can support the project by becoming a Patreon or of course with code contributions.
Stay tuned for more insights about C++ Insight ... . The next post is about lambdas.
Andreas