C++ 最佳实践:通过常量引用或转发引用(又名通用引用)将仅使用(未存储)的 lambda 参数传递给函数

j00*_*0hi 1 c++ c++17

将 lambda 函数作为参数传递给仅使用(但不存储或转发)lambda 函数的函数的最佳(即性能最高、用途最广)的方法是什么?

选项 1:通过常量引用传递它是要走的路吗?

template <typename F>
void just_invoke_func(const F& func) {
    int i = 1;
    func(i);
}
Run Code Online (Sandbox Code Playgroud)

选项2:还是将其作为转发引用(通用引用)传递更好?

template <typename F>
void just_invoke_func(F&& func) {
    int i = 1;
    func(i);
}
Run Code Online (Sandbox Code Playgroud)

后者比前者有什么优势吗?
其中一种解决方案是否有任何危害?

编辑#1:

选项 3:正如Yakk - Adam Nevraumont所指出的,mutablelambdas 不适用于选项 1。那么,另一个采用非常量 l 值引用的版本呢?

template <typename F>
void just_invoke_func(F& func) {
    int i = 1;
    func(i);
}
Run Code Online (Sandbox Code Playgroud)

问题是:选项 2比选项 3有什么优势吗?

编辑#2:

选项 4:已提出另一种按值获取 lambda 的版本。

template <typename F>
void just_invoke_func(F func) {
    int i = 1;
    func(i);
}
Run Code Online (Sandbox Code Playgroud)

Yak*_*ont 6

带有mutable关键字的Lambda 具有非常量operator(),并且在第一种情况下无法编译。

我认为没有理由阻止可变的 lambda 表达式。

在非常量左值引用情况下,右值 lambda 无法编译。

老实说,请考虑按值使用 lambda。如果调用者想要非本地状态,他们可以通过引用捕获,甚至传入std::ref/std::cref包装 lambdas(如果您正在执行不可尾调用优化的递归调用,右值引用可能是明智的)。

template<class F>
void do_something1( F&& f );
template<class F>
void do_something2( F f );
Run Code Online (Sandbox Code Playgroud)

的调用do_something1(lambda)可以通过向一个呼叫被模仿do_something2(std::cref(lambda))(如果不可变)和do_something2(std::ref(lambda))(如果变)。

只有当 lambda 既是可变的又是右值时,这才不起作用;9/10 次这是一个错误(状态被处理的可变 lambda 是不确定的),在剩下的 1/10 次中,您可以解决它。

在另一方面,你不能模仿do_something2do_something1那么容易。

的优点do_something2是编译器在编写 的主体时do_something2,可以准确地知道lambda 的所有状态在哪里。

[x = 0](int& y)mutable{y=++x;}
Run Code Online (Sandbox Code Playgroud)

如果此 lambda 是按值传递的,则编译器在本地知道谁有权访问x. 如果这个 lambda 是通过引用传递的,编译器必须理解调用上下文和它调用的所有代码,以便知道其他人是否x在自己的代码中的任何两个表达式之间进行了修改。

编译器通常可以比外部引用更好地优化值。

至于复制的成本,Lambda 的移动成本通常不高。如果它们是[&]-capture,则它们只是状态方面的一堆引用。如果它们是价值捕获,则很少有它们的状态巨大且难以移动。

在这些情况下,std::refstd::cref无论如何消除该成本。

std::refoperator()通行证,通过对拉姆达,因此函数永远不需要知道它是通过std::ref(lambda))。


n31*_*159 4

如果您尝试采用不同的 lambda 表达式,您会发现 const 引用不能采用可变 lambda 表达式,而可变引用不能采用临时表达式(因此您无法当场定义 lambda)。

按值获取和按通用参考获取适用于所有类型的 lambda。但通用引用更通用(请原谅双关语):如果你有一个想要多次使用的 lambda,但它没有复制构造函数,那么你不能按值获取它,因为你必须移动它并且不能 (或者至少不应该)之后再使用它。

总而言之,我认为通用参考是最简单的方法。

附录:因为您在评论中询问了性能:如果通过转发引用传递与按值传递会产生巨大差异,那么您可能做错了什么。函数对象通常应该是轻量级的。如果您的 lambda 需要花费大量精力来传递值,那么您应该重新设计它。

即使您可以使用转发引用等方式使其工作,但这似乎是一个主要麻烦,并且您很容易犯错误并进行昂贵的复制操作。即使您做得绝对正确并且代码中从未有副本也有两个地方可能会发生这种情况:

  • 其他人的代码:大多数人认为函数对象是轻量级的,因此按值传递它们没有问题。
  • STL:看一下算法头。所有传递到那里的函数对象都是按值获取的(因为 STL 也假设函数对象是轻量级的)。因此,如果您在任何地方使用 STL 算法(众所周知:我们应该尽可能使用它们),那么您的日子就会很糟糕。