C++17 has a powerful template feature – Fold Expressions – that combines with a C++11 feature – Parameter Pack. This allows us to do away with C-style variable arguments (variadic functions). I introduce Lamda Overloading as well, to work with these two concepts in a wonderful way.
First off, the Parameter Pack. This is a template feature that allows a variable amount of parameters to be passed to the template. Here’s a sum function declaration:
template<typename ...Args>
auto sum(Args... args);
In the template area, there are a variable amount of parameters, called Args. In the function area, the arguments accepted are the various parameters, Args. Each element in that pack of parameters will be referred to as, args, inside the function body.
Now to add folding.
template<typename ...Args>
auto sum(Args... args) {
return (args + ...);
}
A fold is contained within parenthesis. Sequentially, left most parameters to right most are included, each called args. Next, each specific parameter, each args, is combined with some operator, in this case, addition. Finally, ellipses signals repeat for each arg.
int i = sum(0,3,5);
Expands to the following, at compile-time:
return (0 + 3 + 5);
So what we’re going to do is write a more modern printf sort of function without using a c variadic function. There are a lot of ways to do this, but because I love lamdas, I want to include lamdas as well. Here we go:
So, in order to vary the generation of strings based on the type of the input parameter, at compile-time, we need to use additional templates. The first thing necessary was an ability to do function overloading with lamdas, hence the LamdaOverload class. This class basically incorporates a bunch of lamdas under one name and overloads the execution operator for each one.
template<typename... Bases>
struct LambdaOverload : public Bases... {
using Bases::operator()...;
LambdaOverload(const Bases&... bases) : Bases(bases)... { }
};
template<typename ...Args>
void new_printf(Args... args) {
auto deduce = LambdaOverload(
[](const char* c) -> std::string { return c; },
[](const std::string& str)->const std::string& { return str; },
[](const auto& t) { return std::to_string(t); }
);
std::string o = (( deduce(args) + " " ) + ...);
OutputDebugStringA(o.c_str());
std::cout << o << std::endl;
}
Next, inside the new_printf function, we define the deduce LamdaOverload object. This object is used to define routes from data types to code-calling. There are different types accepted, and one is a catch-all type which is going to be used for natives that work with the std::to_string function. Note that if the catch-all doesn’t work with std::to_string, an error will be generated.
You can add more overloads to LamdaOverload to accept custom types as well. For instance,
[](const MyClass& c) ->std::string { return c.toString(); }
The call ends up looking like so:
new_printf("the value of x: ", int(x), std::string("and"), 0.1f );