我应该通过 const 引用传递一个 lambda。

Posted

技术标签:

【中文标题】我应该通过 const 引用传递一个 lambda。【英文标题】:Should I pass a lambda by const reference. 【发布时间】:2015-09-25 15:57:31 【问题描述】:

当接受 lambda 作为函数的参数时,我通常使用以下模式(按值传递的模板类):

template <class Function>
void higherOrderFunction(Function f) 
     f();

这是否复制(关闭)参数? 如果是这样,那么通过 const 引用接受 lambda 有什么问题吗?

template <class Function>
void higherOrderFunction(const Function& f) 
  f();

一个简单的测试似乎表明这工作正常,但我想知道是否有任何我应该注意的特殊注意事项。

【问题讨论】:

我相信这不是复制,而是移动。应该足够快并有一个不错的实现。 Lambda 被设计为像这样按值传递 - 所有标准算法都按值获取它们的函子。 【参考方案1】:

如果您通过值传递,您将复制闭包对象(假设您没有定义内联 lambda,在这种情况下它将被移动)。如果 state 的复制成本很高,这可能是不可取的,如果 state 不可复制,则编译失败。

template <class Function>
void higherOrderFunction(Function f);

std::unique_ptr<int> p;
auto l = [p = std::move(p)] ; // C++14 lambda with init capture
higherOrderFunction(l);         // doesn't compile because l is non-copyable 
                                // due to unique_ptr member
higherOrderFunction([p = std::move(p)] ); // this still works, the closure object is moved

如果您通过const 引用,那么您不能将修改其数据成员的mutable lambda 作为参数传递给higherOrderFunction(),因为mutable lambda 有一个非const operator(),你不能在 const 对象上调用它。

template <class Function>
void higherOrderFunction(Function const& f);

int i = 0;
higherOrderFunction([=]() mutable  i = 0; ); // will not compile

最好的选择是使用转发参考。然后higherOrderFunction 可以接受调用者传递的左值或右值。

template <class Function>
void higherOrderFunction(Function&& f) 
     std::forward<Function>(f)();

这允许编译简单的情况以及上面提到的情况。有关为什么应该使用std::forward 的讨论,请参阅this answer。

Live demo

【讨论】:

"如果按值传递,您将复制闭包对象。"这不一定是真的,在最常见的情况下实际上是错误的。通过值传递对右值进行移动,而不是复制,并且 lambdas 最常见的情况是创建 lambda 内联,这应该产生一个右值。您的回答也引出了一个直接的问题:如果转发引用是最好的,为什么 STL 使用值而不是转发引用? (诚​​实的问题) @Nir 同意,更新了答案以明确这一点。至于按值取谓词的 STL 算法,我猜这是因为当时还没有转发引用。无论如何,我想不出在这种情况下不使用转发引用的论据。 公平地说,std 算法按值取函数,人们应该知道,如果他们有一个生命周期有限的高成本函数对象,他们应该std::ref 它。 我的理解是你不需要使用 std::forward(f) 来调用它。我有错吗? @ChrisGuzak 我在最后链接到another answer,这解释了为什么使用std::forward 可以有所作为。【参考方案2】:

一个副本就是一个副本,所以你不能改变原来的,当涉及大量数据时可能会对性能产生一些影响:

#include <iostream>
using namespace std;

template<typename Fn>
void call_value(Fn f)  f(); 
template<typename Fn>
void call_ref(Fn & f)  f(); 
template<typename Fn>
void call_cref(Fn const & f)  f(); 

struct Data 
  Data() 
  Data(Data const &) 
    cout << "copy" << endl;
  
  Data(Data &&) 
    cout << "move" << endl;
  
;

int main(int, char **) 
  Data data;

  auto capref = [&data] () ;
  cout << "capture by value, so we get a ";
  auto capcp = [data] () ;

  cout << " the lambda with a reference ... ";
  call_value(capref);
  cout << " could now be called and mutate .. ";
  call_ref(capref);
  call_cref(capref);
  cout << " but won't, as it had to be declared mutable " << endl;

  cout << "the lambda with an instance: ";
  call_value(capcp);
  cout << "but not ";
  call_ref(capcp);
  call_cref(capcp);
  cout << " the reference versions " << endl;

  bool en = false;
  auto trigger = [en](bool enable = true) mutable 
    if (en) 
      cout << "fire!" << endl;
    
    if (en or enable) 
      en = true;
    
  ;
  cout << "won't shoot" << endl;
  trigger(false);
  call_value(trigger);
  trigger(false);
  call_ref(trigger);
  cout << "and now ... ";
  trigger(false);
  // const ref won't work
  return 0;

看in action。

不要忘记:lambda 只是可调用类的语法糖。 (但非常有帮助)

【讨论】:

【参考方案3】:

STL 不使用模板参数中的函数对象的引用。因此,您很可能也不应该这样做,除非您有非常具体的需求并深入了解为什么 ref 会在您的情况下为您提供帮助,以及这样做的风险是什么。

如果您确实使用引用,但我不确定最终结果是否易于阅读且极易维护,所以即使这样,您也应该寻找替代解决方案,例如在引用捕获的 var 中维护您的状态,并继续按值传递您的 lambda 对象。否则,请至少添加一些 cmets,说明您为什么做了一些复杂且非惯用的事情,而不是标准在其自己的库中到处使用的简单事情。

无论如何,在某些地方,完全转发的通用引用会非常好,并且具有额外复杂性的风险很小:如果函数对象只是简单地转发一次到另一个函数的参数。好吧,这就是为什么你应该坚持尽可能简单的东西,除非你深入了解(并且你希望你的代码的所有未来维护者也会这样做);因为在一般情况下,在通用引用上盲目使用 std::forward 是不安全:例如,在循环体中使用它会非常危险。即便如此,您可能只想在您期望未来的维护者也都知道这一点并且不会以看似简单但不正确的方式扩展代码时这样做。

所以作为一般规则;只有在您非常了解为什么使用它们、它们如何工作的所有细节以及所有相关的潜在风险的情况下,才使用特别棘手的 C++ 功能。在 C++ 中正确使用 C++ 引用是很棘手的,因为如果你犯了任何错误(在数百个潜在错误中),编译器会很乐意为你损坏的程序生成一个二进制文件,大多数时候甚至没有警告告诉你你做错了什么.按值传递的危险性要小得多,所以至少在标准这样做的情况下,你也应该这样做。除非您是专家,并且只与专家合作。

【讨论】:

以上是关于我应该通过 const 引用传递一个 lambda。的主要内容,如果未能解决你的问题,请参考以下文章

将 lambda 作为参数传递 - 通过引用或值?

复制赋值运算符应该通过 const 引用还是按值传递?

通过 const 引用传递对象?

通过引用传递使用 const 有啥意义? C++ [重复]

为啥向量被视为按值传递,即使它是通过 const 引用传递的?

按值传递比通过 const 引用传递更快的经验法则?