Variadic template pack expansion

47,760

Solution 1

One of the places where a pack expansion can occur is inside a braced-init-list. You can take advantage of this by putting the expansion inside the initializer list of a dummy array:

template<typename... Args>
static void foo2(Args &&... args)
{
    int dummy[] = { 0, ( (void) bar(std::forward<Args>(args)), 0) ... };
}

To explain the content of the initializer in more detail:

{ 0, ( (void) bar(std::forward<Args>(args)), 0) ... };
  |       |       |                        |     |
  |       |       |                        |     --- pack expand the whole thing 
  |       |       |                        |   
  |       |       --perfect forwarding     --- comma operator
  |       |
  |       -- cast to void to ensure that regardless of bar()'s return type
  |          the built-in comma operator is used rather than an overloaded one
  |
  ---ensure that the array has at least one element so that we don't try to make an
     illegal 0-length array when args is empty

Demo.

An important advantage of expanding in {} is that it guarantees left-to-right evaluation.


With C++17 fold expressions, you can just write

((void) bar(std::forward<Args>(args)), ...);

Solution 2

Parameter packs can only be expanded in a strictly-defined list of contexts, and operator , is not one of them. In other words, it's not possible to use pack expansion to generate an expression consisting of a series of subexpressions delimited by operator ,.

The rule of thumb is "Expansion can generate a list of ,-separated patterns where , is a list delimiter." Operator , does not construct a list in the grammar sense.

To call a function for each argument, you can use recursion (which is the primary tool in the variadic template programmer's box):

template <typename T>
void bar(T t) {}

void foo2() {}

template <typename Car, typename... Cdr>
void foo2(Car car, Cdr... cdr)
{
  bar(car);
  foo2(cdr...);
}

int main()
{
  foo2 (1, 2, 3, "3");
}

Live example

Solution 3

SHAMELESS COPY [approved by its source]

Parameter packs can only be expanded in a strictly-defined list of contexts, and operator , is not one of them. In other words, it's not possible to use pack expansion to generate an expression consisting of a series of subexpressions delimited by operator ,.

The rule of thumb is "Expansion can generate a list of ,-separated patterns where , is a list delimiter." Operator , does not construct a list in the grammar sense.

To call a function for each argument, you can use recursion (which is the primary tool in the variadic template programmer's box):

#include <utility>

template<typename T>
void foo(T &&t){}

template<typename Arg0, typename Arg1, typename ... Args>
void foo(Arg0 &&arg0, Arg1 &&arg1, Args &&... args){
    foo(std::forward<Arg0>(arg0));
    foo(std::forward<Arg1>(arg1), std::forward<Args>(args)...);
}

auto main() -> int{
    foo(1, 2, 3, "3");
}

USEFUL NON-COPIED INFO

Another thing you probably haven't seen in this answer is use of the && specifier and std::forward. In C++, the && specifier can mean one of 2 things: rvalue-references, or universal references.

I won't go into rvalue-references, but to somebody working with variadic templates; universal references are a god-send.

Perfect Forwarding

One of the uses of std::forward and universal references are perfect forwarding of types to other functions.

In your example, if we pass an int& to foo2 it will be automatically demoted to int because of the signature of the generated foo2 function after template deduction and if you wanted to then forward this arg to another function that would modify it by reference, you will get undesired results (the variable won't be changed) because foo2 will be passing a reference to the temporary created by passing an int to it. To get around this, we specify a forwarding function to take any type of reference to a variable (rvalue or lvalue). Then, to be sure that we pass the exact type passed in the forwarding function we use std::forward, then and only then do we allow the demoting of types; because we are now at the point where it matters most.

If you need to, read more on universal references and perfect forwarding; Scott Meyers is pretty great as a resource.

Solution 4

The C++17 solution to this is really close to your expected code:

template<typename T>
static void bar(T t) {}

template<typename... Args>
static void foo2(Args... args) {
    (bar(args), ...);
}

int main() {
    foo2(1, 2, 3, "3");
    return 0;    
}

This expand the pattern with the comma operator between every expression

// imaginary expanded expression
(bar(1), bar(2), bar(3), bar("3"));

Solution 5

You can use make_tuple for pack expansion as it introduces a context where the , sequence produced by an expansion is valid

make_tuple( (bar(std::forward<Args>(args)), 0)... );

Now, I suspect the unused/unnamed/temporary tuple of zeroes that's produced is detectable by the compiler and optimized away.

Demo

Share:
47,760
Viacheslav  Dronov
Author by

Viacheslav Dronov

Updated on March 16, 2020

Comments

  • Viacheslav  Dronov
    Viacheslav Dronov about 4 years

    I am trying to learn variadic templates and functions. I can't understand why this code doesn't compile:

    template<typename T>
    static void bar(T t) {}
    
    template<typename... Args>
    static void foo2(Args... args)
    {
        (bar(args)...);
    }
    
    int main()
    {
        foo2(1, 2, 3, "3");
        return 0;    
    }
    

    When I compile it fails with the error:

    Error C3520: 'args': parameter pack must be expanded in this context

    (in function foo2).

  • RamblingMad
    RamblingMad over 9 years
    damn, you just bet me to answering this shakes fist but you should probably add perfect forwarding to your answer; that's also a "primary tool in the variadic template programmer's box".
  • Viacheslav  Dronov
    Viacheslav Dronov over 9 years
    Thank you for your answer. I know about recursion implementation. I just want to find workaround to compile code without recursion and new function.
  • RamblingMad
    RamblingMad over 9 years
    @ViacheslavDronov seeing as though you're using templates: you already have a crap-load of functions being generated by the compiler, why not add one on to that list?
  • Angew is no longer proud of SO
    Angew is no longer proud of SO over 9 years
    @CoffeeandCode I officially give you permission to copy my answer and expand on it with adding and explaining perfect forwarding.
  • T.C.
    T.C. over 9 years
    You can easily do the expansion with a dummy array. int dummy[] = { 0, ((void) bar(std::forward<Args>(args)),0)... };.
  • Angew is no longer proud of SO
    Angew is no longer proud of SO over 9 years
    @T.C. That's a great trick, haven't seen that one before. I feel you should actually turn this into an answer.
  • Mooing Duck
    Mooing Duck over 9 years
    One variant I've seen is using expander = int[]; expander{...}; because then the array variable has no name, and it becomes blatantly obvious to the compiler that the array doesn't need to me created or used.
  • Aaron McDaid
    Aaron McDaid almost 9 years
    What's the second 0 for? The one just after the comma operator
  • T.C.
    T.C. almost 9 years
    @AaronMcDaid So that the type of the whole expression is int and matches the array's element type.
  • Aaron McDaid
    Aaron McDaid almost 9 years
    I have noticed that one can write an operator void () (yes, mad, I know). But no, I'm not really saying that casting to void is a bad idea. Just having fun thinking about crazy stuff that can happen when trying to expand a pack with minimal side effects
  • T.C.
    T.C. almost 9 years
    @AaronMcDaid (void) never invokes operator void() (thankfully).
  • Johannes Schaub - litb
    Johannes Schaub - litb about 7 years
    Last time I tried with GCC, it insisted on calling operator void when you casted to void. Therefore, I always write (foo, void(), 0)..., to have it work also for GCC.
  • T.C.
    T.C. about 7 years
    @JohannesSchaub-litb GCC hasn't been doing it since 4.8, I think. The issue with void() is that it still triggers overload resolution, which may cause template instantiations that lead to hard errors.
  • Johannes Schaub - litb
    Johannes Schaub - litb about 7 years
    @T.C. it triggers overload resolution, but it does not really overload-resolve. Because if either the left or right operand of the comma operator is of type void, then the builtin comma operator is used (there's a special case for this). Can you please show an example that has an hard error because of this style of the void trick?
  • T.C.
    T.C. about 7 years
    @JohannesSchaub-litb Right, but the way it works is that you assemble a candidate set, find that nothing is viable, and then default to to the builtin, and the first step can trigger a hard error: godbolt.org/g/31hCVe
  • Johannes Schaub - litb
    Johannes Schaub - litb about 7 years
    @T.C. I was in the impression that if one of the operands is void, then there are no user defined candidates. Looking again, I see that I was wrong.
  • Eric
    Eric almost 5 years
    This doesn't make any guarantees about which order bar is invoked in
  • dfrib
    dfrib over 4 years
    @T.C.: I added a related question to this Q&A that you most definitely have some good input to, if you have the time and interest.
  • user253751
    user253751 over 3 years
    @MooingDuck It is already blatantly obvious to the compiler; using expander{...} makes it obvious to the programmer as well.