可变参数模板扩张

我正在学习可变模板和函数,我不明白为什么这段代码不能编译:

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;
}

当我编译它失败时会出现这样的错误:

错误 C3520: ‘ args’: 参数包必须在此上下文中展开

(功能 foo2)。

53221 次浏览

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

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 foo20 lvalue). Then, to be sure that we pass the exact type passed in the forwarding function we use std::forward, then and foo21 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.

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)), ...);

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

This is a full example, based on the answers here.

Example to reproduce console.log as seen in JavaScript:

Console console;
console.log("bunch", "of", "arguments");
console.warn("or some numbers:", 1, 2, 3);
console.error("just a prank", "bro");

Filename e.g. js_console.h:

#include <iostream>
#include <utility>


class Console {
protected:
template <typename T>
void log_argument(T t) {
std::cout << t << " ";
}
public:
template <typename... Args>
void log(Args&&... args) {
int dummy[] = { 0, ((void) log_argument(std::forward<Args>(args)),0)... };
cout << endl;
}


template <typename... Args>
void warn(Args&&... args) {
cout << "WARNING: ";
int dummy[] = { 0, ((void) log_argument(std::forward<Args>(args)),0)... };
cout << endl;
}


template <typename... Args>
void error(Args&&... args) {
cout << "ERROR: ";
int dummy[] = { 0, ((void) log_argument(std::forward<Args>(args)),0)... };
cout << endl;
}
};

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"));