可变参数模板包扩展

Posted

技术标签:

【中文标题】可变参数模板包扩展【英文标题】:Variadic template pack expansion 【发布时间】:2014-10-30 02:05:59 【问题描述】:

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

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中)。

【问题讨论】:

如果Args 为空怎么办? 【参考方案1】:

可能发生包扩展的地方之一是 braced-init-list。您可以通过将扩展放在虚拟数组的初始化列表中来利用这一点:

template<typename... Args>
static void foo2(Args &&... args)

    int dummy[] =  0, ( (void) bar(std::forward<Args>(args)), 0) ... ;

更详细地解释初始化器的内容:

 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.

中扩展的一个重要优势是它保证了从左到右的评估。


使用 C++17 fold expressions,您可以编写

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

【讨论】:

我见过的一个变体是using expander = int[]; expander...;,因为数组变量没有名称,编译器很明显不需要我创建或使用数组。跨度> 第二个0 是干什么用的?逗号运算符之后的那个 @AaronMcDaid 使得整个表达式的类型为int 并匹配数组的元素类型。 我注意到一个人可以写一个operator void ()(是的,疯了,我知道)。但是,不,我并不是说投射到void 是一个坏主意。只是想着在尝试以最小的副作用扩展包时可能发生的疯狂事情很有趣 @AaronMcDaid (void) never invokes operator void()(谢天谢地)。【参考方案2】:

参数包只能在严格定义的上下文列表中展开,运算符, 不是其中之一。换句话说,不可能使用包扩展来生成由一系列由运算符, 分隔的子表达式组成的表达式。

经验法则是“扩展可以生成, 分隔模式的列表,其中,列表 分隔符。”运算符, 不构造语法意义上的列表。

要为每个参数调用一个函数,您可以使用递归(这是可变参数模板程序员的主要工具):

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

【讨论】:

该死,你只是打赌我会回答这个握拳,但你可能应该在你的答案中添加完美转发;这也是“可变参数模板程序员盒子中的主要工具”。 感谢您的回答。我知道递归实现。我只是想找到解决方法来编译没有递归和新函数的代码。 @ViacheslavDronov 看起来好像你在使用模板:你已经有一大堆由编译器生成的函数,为什么不在这个列表中添加一个呢? @CoffeeandCode 我正式允许您复制我的答案并通过添加和解释完美转发来扩展它。 您可以使用虚拟数组轻松进行扩展。 int dummy[] = 0, ((void) bar(std::forward&lt;Args&gt;(args)),0)... ;.【参考方案3】:

SHAMELESS COPY [approved by its source]

参数包只能在严格定义的上下文列表中展开,运算符, 不是其中之一。换句话说,不可能使用包扩展来生成由一系列由运算符, 分隔的子表达式组成的表达式。

经验法则是“扩展可以生成, 分隔模式的列表,其中, 是列表分隔符。”运算符, 不构造语法意义上的列表。

要为每个参数调用一个函数,您可以使用递归(这是可变参数模板程序员的主要工具):

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


有用的非复制信息

您可能在此答案中没有看到的另一件事是使用&amp;&amp; 说明符和std::forward。在 C++ 中,&amp;&amp; 说明符可以表示以下两种情况之一:右值引用或通用引用。

我不会介绍右值引用,而是介绍使用可变参数模板的人;通用引用是天赐之物。

完美转发

std::forward 和通用引用的用途之一是将类型完美地转发给其他函数。

在您的示例中,如果我们将int&amp; 传递给foo2,它将自动降级为int,因为模板扣除后生成的foo2 函数的签名,如果您想转发此@ 987654338@ 到另一个通过引用修改它的函数,你会得到不想要的结果(变量不会改变),因为foo2 将传递一个对通过传递一个int 给它创建的临时对象的引用。为了解决这个问题,我们指定了一个转发函数,以将任何类型的引用指向一个变量(右值左值)。然后,为了确保我们传递在转发函数中传递的确切类型,我们使用std::forward,然后 only 然后我们是否允许降级类型;因为我们现在正处于最重要的阶段。

如果需要,请阅读 universal references 和 perfect forwarding 的更多信息; Scott Meyers 作为一种资源非常棒。

【讨论】:

【参考方案4】:

对此的 C++17 解决方案与您预期的代码非常接近:

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;    

这会在每个表达式之间使用逗号运算符扩展模式

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

【讨论】:

【参考方案5】:

您可以使用make_tuple 进行包扩展,因为它引入了一个上下文,其中由扩展产生的, 序列是有效的

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

现在,我怀疑生成的未使用/未命名/临时零元组可以被编译器检测并优化掉。

Demo

【讨论】:

这不能保证bar被调用的顺序【参考方案6】:

这是一个完整的示例,基于此处的答案。

重现 console.log 的示例,如 javascript 中所示:

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

文件名,例如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;
    
;

【讨论】:

以上是关于可变参数模板包扩展的主要内容,如果未能解决你的问题,请参考以下文章

C++11 ——— 可变参数模板

C++11 ——— 可变参数模板

为啥编译器不能通过逗号运算符扩展可变参数模板的参数?

可变参数模板到数组访问的无递归扩展

第20课 可变参数模板_模板参数包和函数参数包

使用可变参数模板进行扩展[重复]