Variadic templates will enable the rewriting of certain kind of functions into cleaner, type-safe versions. It is the case of printf
, as the example given on Wikipedia:
void printf(const char *s)
{
while (*s) {
if (*s == '%' && *(++s) != '%')
throw std::runtime_error("invalid format string: missing arguments");
std::cout << *s++;
}
}
template<typename T, typename... Args>
void printf(const char *s, T value, Args... args)
{
while (*s) {
if (*s == '%' && *(++s) != '%') {
std::cout << value;
++s;
printf(s, args...); // call even when *s == 0 to detect extra arguments
return;
}
std::cout << *s++;
}
throw std::logic_error("extra arguments provided to printf");
}
But... As far as I understand templates, they imply code duplication for each type combination. So the variadic version of the above printf
s would be copied many times. This could be terrible for large functions or classes.
Are variadic template as perilous as standard templates for code duplication? If yes, can the inheritance trick still help?
How does template cause the code bloat in C++? Code bloat occurs because compilers generate code for all templated functions in each translation unit that uses them. Back in the day, the duplicate code was not consolidated, which resulted in "code bloat". These days, the duplicate code can be removed at link time.
Code bloat is the production of code that is perceived as unnecessarily long, slow, or otherwise wasteful of resources. It is a problem in Software Development which makes the length of the code of software long unnecessarily.
With the variadic templates feature, you can define class or function templates that have any number (including zero) of parameters. To achieve this goal, this feature introduces a kind of parameter called parameter pack to represent a list of zero or more parameters for templates.
Variadic templates are class or function templates, that can take any variable(zero or more) number of arguments. In C++, templates can have a fixed number of parameters only that have to be specified at the time of declaration. However, variadic templates help to overcome this issue.
The short answer is: the "you only pay for what you use" principle still applies exactly as before.
The longer answer can be seen by comparing the generated code for two hypothetical implementations e.g.
#include <iostream>
template <typename T>
void func1(T& v) {
v = -10;
}
template <typename T1, typename T2>
void func1(T1& v1, T2& v2) {
func1(v1); func1(v2);
}
// More unused overloads....
template <typename T1, typename T2, typename T3>
void func1(T1& v1, T2& v2, T3& v3) {
func1(v1); func1(v2); func1(v3);
}
int main() {
double d;
int i;
func1(d);
func1(i);
std::cout << "i=" << i << ", d=" << d << std::endl;
func1(d,i);
std::cout << "i=" << i << ", d=" << d << std::endl;
}
With a modern compiler this pretty much reduces to exactly what you'd have written if you wanted to avoid templates all together. In this "traditional" C++03 templated code my version of g++ inlines (in the compiler, not keyword sense) the whole lot and there's no obvious hint that the initializations are done via reference in a template function, several times, in different ways.
Compared with the equivalent variadic approach:
#include <iostream>
#include <functional>
void func1() {
// end recursion
}
template <typename T, typename ...Args>
void func1(T& v, Args&... args) {
v = -10;
func1(args...);
}
int main() {
double d;
int i;
func1(d);
func1(i);
std::cout << "i=" << i << ", d=" << d << std::endl;
func1(d,i);
std::cout << "i=" << i << ", d=" << d << std::endl;
}
This also produces almost identical code - some of the labels and mangled names are different as you'd expect, but the diff of the generated asm produced by g++ -Wall -Wextra -S
(a 4.7 snapshot) has no significant differences. The compiler basically is writing all of the overloads your program requires on the fly and then optimizing as before.
The following non template code also produces almost identical output:
#include <iostream>
#include <functional>
int main() {
double d;
int i;
d= -10; i=-10;
std::cout << "i=" << i << ", d=" << d << std::endl;
d= -10; i=-10;
std::cout << "i=" << i << ", d=" << d << std::endl;
}
Here again the only noticeable differences are the labels and symbol names.
The point is a modern compiler can do "what's right" without much hassle in template code. If what you're expressing is simple underneath all the template mechanics the output will be simple. If it's not then the output will be more substantial, but so would the output be if you'd avoided templates entirely.
Where this gets interesting (in my view) however is this: all of my statements were qualified with something like "with an decent modern compiler". If you're writing variadic templates you can almost be certain that what you're using to compile is a decent modern compiler. No clunky old relic compilers support variadic templates.
It could certainly be a problem. One thing that could help is to factor out the common parts:
const char *process(const char *s)
{
while (*s) {
if (*s == '%' && *(++s) != '%') {
++s;
return s;
}
std::cout << *s++;
}
throw std::logic_error("extra arguments provided to printf");
}
template<typename T>
inline const char *process(const char *s,T value)
{
s = process(s);
std::cout << value;
return s;
}
template<typename T, typename... Args>
inline void printf(const char *s, T value, Args... args)
{
printf(process(s,value),args...);
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With