【问题标题】:Should templated functions take lambda arguments by value or by rvalue reference?模板化函数应该通过值还是通过右值引用来获取 lambda 参数?
【发布时间】:2012-09-14 22:38:59
【问题描述】:

C++11 模式下的 GCC 4.7 允许我定义一个采用 lambda 的函数,有两种不同的方式:

// by value
template<class FunctorT>
void foo(FunctorT f) { /* stuff */ }

还有:

// by r-value reference
template<class FunctorT>
void foo(FunctorT&& f) { /* stuff */ }

但不是:

// by reference
template<class FunctorT>
void foo(FunctorT& f) { /* stuff */ }

我知道我可以取消模板函数并只使用 std::functions 代替,但 foo 很小且内联,我想给编译器最好的机会来内联它对 f 的调用里面。在前两个中,如果我特别知道我正在传递 lambda,这对于性能来说更可取,为什么不允许将 lambda 传递给最后一个?

【问题讨论】:

    标签: c++ templates lambda c++11 g++


    【解决方案1】:

    FunctorT&amp;&amp; 是一个通用参考,可以匹配任何东西,而不仅仅是右值。这是在 C++11 模板中传递内容的首选方式,除非您绝对需要副本,因为它允许您使用完美转发。通过std::forward&lt;FunctorT&gt;(f) 访问该值,这将使f 再次成为右值(如果它是以前的),否则将其保留为左值。阅读更多关于转发问题的herestd::forward 并阅读here 了解std::forward 如何真正工作的分步指南。 This 也是一本有趣的书。

    FunctorT&amp; 只是一个简单的左值引用,您不能将临时变量(lambda 表达式的结果)绑定到它。

    【讨论】:

    • @Joseph:首先,如果您将临时值传递给第一个函数,则临时值将被移动到参数中,而不是复制。只有在您传递 lvalu 时才会复制它。对于通用引用,您既不会复制也不会移动,因为它是直接绑定到临时(或左值或传递的任何内容)的 reference。很多时候,无论如何都会有复制省略,根本看不到任何复制/移动(具有副作用的复制/移动ctor可以被省略,不会发生副作用,这是标准特别允许的)。跨度>
    • @JosephGarvin:std::forward 如果您不进一步通过它,则没有实际意义:不,不是。使用operator() 对左值和右值都具有ref-qualified(参见here)的任何函子(不是特定的lambdas)进行成像,而右值则做一些不同的事情。如果您在传入临时值时不使用std::forward,您将永远调用左值重载。基本上,您确实将仿函数 - 传递给它自己的成员函数。 ;)
    • 所以,如果我理解正确的话,我现在在所有有模板类型参数的模板代码的地方我都应该使用 &&,并且每次我使用我不明确想要副本的参数时我应该使用 std::forward?这似乎是一吨额外的样板......
    • @Joseph:对于全面的通用性,是的,这就是代码的路径。除非您明确需要只读引用,否则您当然可以使用T const&amp;
    • @JosephGarvin 认为template&lt;typename F&gt; void foo(F f) { f(); }template&lt;typename F&gt; void foo(F&amp;&amp; f) { std::forward&lt;F&gt;(f)(); } 的“足够好”版本并没有错。除了我认为病态的情况外,您不会失去实际功能 - 您可能会做一些不必要的工作,但无论哪种方式都会得到相同的结果。
    【解决方案2】:

    当你创建一个 lambda 函数时,你会得到一个临时对象。您不能将临时绑定到非常量左值引用。实际上,您不能直接创建引用 lambda 函数的左值。

    当您使用 T&amp;&amp; 声明函数模板时,如果您将 const 对象传递给函数,则函数的参数类型将为 T const&amp;,如果您传递非 const 左值对象,则为 T&amp;给它,T 如果你暂时传递它。也就是说,当传递一个临时函数声明时,函数声明将采用一个 r 值引用,该引用可以在不移动对象的情况下传递。当通过值显式传递参数时,临时对象在概念上被复制或移动,尽管此复制或移动通常被省略。如果您只将临时对象传递给您的函数,则前两个声明会做同样的事情,尽管第一个声明可能会引入移动或复制。

    【讨论】:

    • @Nicol:T&amp;&amp; 中的T 将是一个简单的T(又名int),而不是T&amp;&amp;(又名int&amp;&amp;)。引用折叠仅适用于左值,这将使T&amp;&amp; 中的T 产生T&amp;
    • Dietmar 确实 听起来最终产品将是T,尤其是最后一句话。对于右值,前两个声明将不同,因为一个会引起另一个不会发生的移动。
    • 好吧,我的意思是我写的,但我写的似乎确实不准确,尽管在实践中它可能并不重要(因为通常会忽略复制或移动)。我会相应地更新响应。
    • “我知道你认为你理解你以为我说的,但我不确定你是否意识到你听到的不是我的意思。”——艾伦·格林斯潘在演示过程中回答学生提出的问题。
    【解决方案3】:

    这是一个很好的问题——第一部分:按值传递或使用转发。我认为第二部分(以FunctorT&amp; 作为论点)已经得到了合理的回答。

    我的建议是:仅在函数对象已知时使用转发,提前修改其闭包(或捕获列表)中的值。最好的例子:std::shuffle。它需要一个统一随机数生成器(一个函数对象),每次调用生成器都会修改其状态。函数对象被转发到算法中。

    在所有其他情况下,您应该更喜欢按值传递。这不会阻止您通过引用捕获本地变量并在您的 lambda 函数中修改它们。这将像您认为的那样工作。正如 Dietmar 所说,复制不应该有任何开销。内联也将适用,并且可以优化引用。

    【讨论】:

    • 当你说不应该有复制开销时,你的意思是在实践中副本将被优化或语义上不会复制?
    • 副本应该被省略。优化了。
    • 唯一可以省略的按值复制是概念上在调用站点创建的右值,可以将其构造为作为参数。除此之外,按值参数的副本,例如级联到其他函数调用,绝对不能省略。每个函数都必须有自己的副本,该副本必须与原始函数分开:它不能跟踪对其源的并发更改;对于非const 按值参数,它必须相反是可更改的,而不影响原始参数。所以在这里,副本是保证的。始终使用转发参考。它要么是免费的,要么是保存无意义的副本。为什么要付钱?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-07-17
    • 2019-08-28
    • 1970-01-01
    • 2022-01-21
    • 1970-01-01
    • 1970-01-01
    • 2019-04-20
    相关资源
    最近更新 更多