常识知道lambda函数是引擎盖下的函子。
在this video(@约45:43),Bjarne说:
我提到lambda会转换为函数对象,如果方便的话会转换为函数
我可以看到这是一个编译器优化(即它不会改变lambda作为未命名的仿函数的感知,这意味着例如lambda仍然不会重载)但是否有任何规则指定何时这是适用吗
我理解术语翻译(这就是我要问的)与转换无关(我不会问lambdas是否可以转换为函数ptr等)。通过翻译我的意思是“将lambda表达式编译成函数而不是函数对象”。
作为mentioned in cppreference: lambda表达式构造一个未命名的prvalue临时对象,该对象具有唯一的非命名非联合非聚合类型,称为闭包类型。
问题是:这个对象可以被省略并且具有普通功能吗?如果是,那么何时以及如何?
注意:我想一个这样的规则是“不捕捉任何东西”,但我找不到任何可靠的来源来确认它
答案 0 :(得分:7)
来自 Lambda表达式§5.1.2p6(草案N4140)
没有lambda-capture的非泛型lambda表达式的闭包类型具有公共非虚拟非 显式const转换函数指向函数与C ++语言链接具有相同的 参数和返回类型作为闭包类型的函数调用操作符。
答案 1 :(得分:7)
标准报价已经发布,我想补充一些例子。 只要没有捕获的变量,就可以将lambda指定给函数指针:
法律:
int (*f)(int) = [] (int x) { return x + 1; }; // assign lambda to function pointer
int z = f(3); // use the function pointer
非法:
int y = 5;
int (*g)(int) = [y] (int x) { return x + y; }; // error
法律:
int y = 5;
int z = ([y] (int x) { return x + y; })(2); // use lambda directly
(编辑)
既然我们不能向Bjarne询问他的意思,我想尝试一些解释。
“翻译”意为“转换”
这是我最初的理解,但现在很清楚,问题不在于这个可能的含义。
“翻译”在C ++标准中使用,意思是“编译”(或多或少)
正如Sebastian Redl已经评论过的那样,二进制级别上没有函数对象。只有操作码和数据,标准没有谈论或指定任何二进制格式。
“翻译”意思是“在语义上等同”
这意味着如果A和B在语义上是等价的,则生成的A和B的二进制代码可以是相同的。我的其余部分使用了这种解释。
关闭由两部分组成:
这相当于一个仿函数,如问题中所述。
Functors 可以看作是对象的子集,因为它们有代码和数据,但只有一个成员函数:调用运算符。因此,闭包可以被视为在语义上等同于受限形式的对象。
另一方面,功能没有与之关联的数据。当然有参数,但这些参数必须由调用者提供,并且可以从一个调用更改为另一个调用。这是闭包的语义差异,其中绑定变量不能更改,也不由调用者提供。
成员函数不是独立的东西,因为它没有它的对象就无法工作,所以我认为这个问题涉及一个独立的函数。
所以不,lambda在语义上通常不等同于函数。
有一个明显的特殊情况,一个没有捕获变量的lambda,其中functor只包含代码,这相当于一个函数。
但是,lambda可以说在语义上等同于 set 的函数。每个可能的闭包(绑定变量的值/引用的不同组合)将等同于该集合中的一个函数。
当然,只有当绑定变量只能有一组非常有限的值/只引用几个不同的变量(如果有的话)时,这才有用。
例如,我认为没有理由为什么编译器不能将以下两个片段视为(几乎*)等效:
void Test(bool cond, int x)
{
int y;
if(cond) y = 5;
else y = 3;
auto f = [y](int x) { return x + y; };
// more code that
// uses f
}
一个聪明的编译器可以看到y只能有值5或3,并且编译好像它会像这样编写:
int F1(int x)
{
return x + 5;
}
int F2(int x)
{
return x + 3;
}
void Test(bool cond, int x)
{
int (*f)(int);
if(cond) f = F1;
else f = F2;
// more code that
// uses f
}
(*)当然这取决于more code that uses f
到底做了什么。
另一个(可能更好)的例子是lambda,它总是通过引用绑定同一个变量。然后,只有一个可能的闭包,因此它等同于一个函数,如果函数可以通过其他方式访问该变量,而不是将其作为参数传递。
另一个可能有用的观察是询问
这个对象[闭包]可以被省略并且具有普通函数吗?如是, 然后何时以及如何?
或多或少与询问何时以及如何在没有该对象的情况下使用成员函数相同。由于lambdas是仿函数,而仿函数是对象,因此这两个问题密切相关。 lambda的绑定变量对应于对象的数据成员,lambda body对应于成员函数的主体。
答案 2 :(得分:6)
TLDR:如果你只使用lambda将其转换为函数指针(并且只通过该函数指针调用它),则省略闭包对象总是有利可图的。实现这一点的优化是内联和消除代码。如果您确实使用lambda本身,仍然可以优化闭包,但需要更积极的过程间优化。
我现在将尝试展示它是如何工作的。我会在我的例子中使用GCC,因为我对它更熟悉。其他编译器应该做类似的事情。
考虑以下代码:
#include <stdio.h>
typedef int (* fnptr_t)(int);
void use_fnptr(fnptr_t fn)
{
printf("fn=%p, fn(1)=%d\n", fn, fn(1));
}
int main()
{
auto lam = [] (int x) { return x + 1; };
use_fnptr((fnptr_t)lam);
}
现在,我编译它并转储中间表示(对于6之前的版本,你应该添加-std=c++11
):
g++ test.cc -fdump-tree-ssa
稍微清理和编辑(为简洁起见)转储看起来像这样:
// _ZZ4mainENKUliE_clEi
main()::<lambda(int)> (const struct __lambda0 * const __closure, int x)
{
return x_1(D) + 1;
}
// _ZZ4mainENUliE_4_FUNEi
static int main()::<lambda(int)>::_FUN(int) (int D.2780)
{
return main()::<lambda(int)>::operator() (0B, _2(D));
}
// _ZZ4mainENKUliE_cvPFiiEEv
main()::<lambda(int)>::operator int (*)(int)() const (const struct __lambda0 * const this)
{
return _FUN;
}
int main() ()
{
struct __lambda0 lam;
int (*<T5c1>) (int) _3;
_3 = main()::<lambda(int)>::operator int (*)(int) (&lam);
use_fnptr (_3);
}
也就是说,lambda有2个成员函数:函数调用操作符和转换操作符以及一个静态成员函数_FUN,它只调用设置为零的this
的lambda。 main
调用转换运算符并将结果传递给use_fnptr - 与源代码中编写的完全相同。
我可以写:
extern "C" int _ZZ4mainENKUliE_clEi(void *, int);
int main()
{
auto lam = [] (int x) { return x + 1; };
use_fnptr((fnptr_t)lam);
printf("%d %d %d\n", lam(10), _ZZ4mainENKUliE_clEi(&lam, 11), __lambda0::_FUN(12));
printf("%p %p\n", &__lambda0::_FUN, (fnptr_t)lam);
return 0;
}
该程序输出:
fn=0x4005fc, fn(1)=2
11 12 13
0x4005fc 0x4005fc
现在,我认为很明显,编译器应该将lambda(_ZZ4mainENKUliE_clEi)内联到_FUN
(_ZZ4mainENUliE_4_FUNEi),因为_FUN
是唯一的调用者。并将operator int (*)(int)
内联到main
(因为此运算符只返回一个常量)。当使用优化(-O)进行编译时,GCC就是这样做的。你可以这样检查:
g++ test.cc -O -fdump-tree-einline
转储文件:
// Considering inline candidate main()::<lambda(int)>.
// Inlining main()::<lambda(int)> into static int main():<lambda(int)>::_FUN(int).
static int main()::<lambda(int)>::_FUN(int) (int D.2822)
{
return _2(D) + 1;
}
闭包对象消失了。现在,一个更复杂的情况,当使用lambda本身时(不是函数指针)。考虑:
#include <stdio.h>
#define PRINT(x) printf("%d", (x))
#define PRINT1(x) PRINT(x); PRINT(x); PRINT(x); PRINT(x);
#define PRINT2(x) do { PRINT1(x) PRINT1(x) PRINT1(x) PRINT1(x) } while(0)
__attribute__((noinline)) void use_lambda(auto t)
{
t(1);
}
int main()
{
auto lam = [] (int x) { PRINT2(x); };
use_lambda(lam);
return 0;
}
GCC不会内联lambda,因为它相当庞大(这就是我使用printf的原因):
g++ test2.cc -O2 -fdump-ipa-inline -fdump-tree-einline -fdump-tree-esra
早期的内联转储:
Considering inline candidate main()::<lambda(int)>
will not early inline: void use_lambda(auto:1) [with auto:1 = main()::<lambda(int)>]/16->main()::<lambda(int)>/19, growth 46 exceeds --param early-inlining-insns
但&#34;早期的过程间标量替换聚合物&#34; pass会做我们想做的事:
;; Function main()::<lambda(int)> (_ZZ4mainENKUliE_clEi, funcdef_no=14, decl_uid=2815, cgraph_uid=12, symbol_order=12)
IPA param adjustments: 0. base_index: 0 - __closure, base: __closure, remove_param
1. base_index: 1 - x, base: x, copy_param
未使用第一个参数(即闭包),它将被删除。不幸的是,过程间SRA无法优化远离间接,这是为捕获的值引入的(尽管有些情况下显然有利可图),因此仍有一些增强空间。
答案 3 :(得分:1)
要提供另一种见解,请在编译以下代码段时查看clang生成的代码:
int (*f) = []() { return 0; }
如果你用以下代码编译它:
clang++ -std=c++11 -S -o- -emit-llvm a.cc
您获得了lambda定义的以下LLVM字节码:
define internal i32 @"_ZNK3$_0clEv"(%class.anon* %this) #0 align 2 {
%1 = alloca %class.anon*, align 8
store %class.anon* %this, %class.anon** %1, align 8
%2 = load %class.anon** %1
ret i32 0
}
define internal i32 @"_ZN3$_08__invokeEv"() #1 align 2 {
%1 = call i32 @"_ZNK3$_0clEv"(%class.anon* undef)
ret i32 %1
}
第一个函数采用%class.anon*
的实例并返回0:它是调用运算符。第二个创建此类的实例(undef
),然后调用其调用运算符并返回值。
使用-O2
编译时,整个lambda变为:
define internal i32 @"_ZN3$_08__invokeEv"() #0 align 2 {
ret i32 0
}
这是一个返回0的单个函数。
我提到lambda会转换为函数对象,如果方便的话会转换为函数
这正是铿锵的行为!它将lambda转换为函数对象,并在可能的情况下将其优化为函数。
答案 4 :(得分:0)
不,它无法完成。 Lambdas被定义为仿函数,我不会在这里看到as-if规则。
[C++14: 5.1.2/6]:
没有 lambda-capture 的非泛型 lambda-expression 的闭包类型具有公共非虚拟非显式const转换函数指向具有与闭包类型的函数调用操作符相同的参数和返回类型的C ++语言链接(7.5)的函数。 [..]
......接下来是通用lambda的类似措辞。