Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C++17

C++ 17 variadic template pack to runtime

4.68/5 (8 votes)
7 Jan 2018CPOL2 min read 20.9K  
Reduce function recursion with variadic templates

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:

 

C++
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...); // call even when *s == 0 to detect extra arguments
                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"); // means that args is unpacked as such.

 

 

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):

C++
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);
 // Hello Jack how are you today? I have 32 eggs and your height is 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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)