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

时间:2015-07-07 23:00:16

标签: c++ c++11 lambda closures pass-by-reference

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

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

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

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

一个简单的测试似乎表明这种方法很好,但我想知道是否有任何我需要注意的特殊注意事项。

3 个答案:

答案 0 :(得分:13)

如果你传递值,你将复制闭包对象(假设你没有定义lambda内联,在这种情况下它将被移动)。如果复制状态昂贵,则可能不合需要,如果状态不可复制,则无法编译。

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

答案 1 :(得分:1)

副本是副本,因此您无法改变原始副本,在涉及大量数据时可能会对性能产生一些影响:

#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

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

答案 2 :(得分:1)

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

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

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

作为一般规则;如果您非常了解为什么使用它们,它们如何工作的所有细节以及所有相关的潜在风险,那么只使用特别棘手的C ++功能。在C ++中正确使用C ++引用是很棘手的,因为如果你犯了任何错误(在数百个潜在的错误中),编译器将很乐意为你破碎的程序生成一个二进制文件,大多数情况下甚至没有警告你告诉你已经做了一些麻烦。通过价值传递是不那么危险的,所以至少在标准这样做的情况下,你也应如此。除非你是专家,否则只与专家合作。