Introduction
In my previous C++ 17 article here I didn't like much std::any. However I 've thought of an interesting trick that can reduce the recursion needed in variadic template functions.
C++ 11 and C++ 17
First, the old way. From Cake Processor good article about variadic printf we have this:
void safe_printf(const char *s)
{
while (*s) {
if (*s == '%') {
if (*(s + 1) == '%') {
++s;
}
else {
throw std::runtime_error("invalid format string: missing arguments");
}
}
std::cout << *s++;
}
}
template<typename T, typename... Args>
void safe_printf(const char *s, T value, Args... args)
{
while (*s) {
if (*s == '%') {
if (*(s + 1) == '%') {
++s;
}
else {
std::cout << value;
safe_printf(s + 1, args...); return;
}
}
std::cout << *s++;
}
throw std::logic_error("extra arguments provided to printf");
}
Two functions. The variadic template one and the final const char* one, that is called last when all the parameters in the pack have expanded. The most important difficuly is the compile-level recursion that must occur in all such templates.
However, remember that the parameter pack expands parameters with comma separator:
template <typename ... Args>
void foo(Args ... args)
{
}
foo(1,2,3,"hello");
Why not use the initializer_list to store these values in a vector, so they can be accessed in run time using a for loop?
template <typename ... Args>
void foo(Args ... args)
{
std::vector<std::any> a = {args ...};
}
Pretty cool. The fact that the items are stored in std::any means that anything can be passed to it, and it can now be accessed in runtime. Note that this works because std::any is not itself a template; It stores the contained object as a generic pointer and tests via typeinfo if it matches the type passed to it. By the way I believe that type testing should be relaxed, for example if you have a std::any that contains an int, why not returning it with any_cast<long>()?
Now the new printf in one function (ok one would loop the vector for efficience, but now it doesn't matter):
template<typename ... many>
void safe_printf2(const char *s, many ... args)
{
vector<any> a = {args ...};
while (*s) {
if (*s == '%') {
if (*(s + 1) == '%') {
++s;
}
else {
if (a.empty())
throw std::logic_error("Fewer arguments provided to printf");
if (a[0].type() == typeid(string)) cout << any_cast<string>(a[0]);
if (a[0].type() == typeid(int)) cout << any_cast<int>(a[0]);
if (a[0].type() == typeid(double)) cout << any_cast<double>(a[0]);
a.erase(a.begin());
s++;
}
}
std::cout << *s++;
}
}
int main()
{
safe_printf2("Hello % how are you today? I have % eggs and your height is %","Jack"s, 32,5.7);
}
Generally, the use of vector<any> allows you to expand a parameter pack to runtime and manipulate it with a for loop.
Of course, the initializer list makes a copy of all the parameters. So you may want to use pointers:
vector<any> a = {&args...};
if (a[0].type() == typeid(string*)) cout << *any_cast<string*>(a[0]);
or, references, with the aid of std::ref and std::reference_wrapper (I prefer pointers. Besides, the std::reference_wrapper takes longer to write, longer to understand and it uses internally pointers anyway since it can't use anything else. Shame on me):
vector<any> a = {std::ref(args)...};
if (a[0].type() == typeid(std::reference_wrapper<string>)) cout << any_cast<std::reference_wrapper<string>>(a[0]).get();
History
06 Jan 2018 : First Release