提问人:OrenIshShalom 提问时间:9/26/2023 更新时间:9/26/2023 访问量:53
如何在 lambda 表达式调用中实际发送闭包进行评估
How are closures actually sent for evaluation in lambda expression calls
问:
我试图了解 lambda 表达式调用的实际闭包是如何发送的:
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char **argv)
{
int *A = (int *) malloc((argc - 1) * sizeof(int));
const auto myLovelyLambda = [=]()
{
// ++++ captured
for (auto i=0;i<argc-1;i++) {
A[i] = atoi(argv[i+1]);
// + ++++ captured
// |
// +- captured
}
};
myLovelyLambda();
for (int i=0;i<argc-1;i++) {
printf("%d\n", A[i]);
}
return 0;
}
当我检查生成的机器代码时,我看到捕获的实体在堆栈上传递:
$ clang --std=c++17 -g -O0 main.cpp -o main
$ objdump -S -D main > main.asm
$ sed -n "22,31p" main.asm
; const auto myLovelyLambda = [=]()
100003e6c: b85f83a8 ldur w8, [x29, #-8]
100003e70: 910043e0 add x0, sp, #16
100003e74: b90013e8 str w8, [sp, #16] // <--- captured
100003e78: f85e83a8 ldur x8, [x29, #-24]
100003e7c: f9000fe8 str x8, [sp, #24] // <--- captured
100003e80: f85f03a8 ldur x8, [x29, #-16]
100003e84: f90013e8 str x8, [sp, #32]. // <--- captured
; myLovelyLambda();
100003e88: 9400001c bl 0x100003ef8 <__ZZ4mainENK3$_0clEv>
我是否可以控制编译器如何管理此闭包移动?
答:
4赞
Jan Schultke
9/26/2023
#1
您必须在概念上将闭包类型的对象的初始化和函数调用分开。lambda 表达式具有相应的闭包类型。在你的情况下,会翻译成这样的东西:myLovelyLambda
// note: this is not a completely accurate representation, it's just for exposition
class __lambda {
private:
int argc;
char** argv;
int* A;
public:
void operator()() const noexcept {
for (auto i=0;i<argc-1;i++) {
A[i] = atoi(argv[i+1]);
}
};
};
请注意,根据 [expr.prim.lambda] p10,闭包类型中 、 和 的顺序未指定:argc
argv
A
对于通过复制捕获的每个实体,将在闭包类型中声明一个未命名的非静态数据成员。 这些成员的声明顺序未指定。
然后,初始化和调用将进行如下转换:
// const auto myLovelyLambda = [=]() { ... };
const auto myLovelyLambda = __lambda{argc, argv, A};
myLovelyLambda();
我是否可以控制编译器如何管理此闭包移动?
没有。您不能在 lambda 中捕获,因此编译器可以自由地对对象中捕获的初始化进行重大转换和重新排序。它还可以通过内联完全优化 lambda,使组件无法区分直接具有您的循环。volatile
for
main
此外,还未指定 lambda 捕获在闭包对象中的初始化顺序,以及它们被销毁的顺序。请参阅 C++11:lambda 捕获的销毁顺序是什么?。 最后,你可以让编译器来弄清楚。在您的示例中,您不需要严格控制捕获顺序。
评论
myLovelyLambda
只是一个常规对象,其成员(即捕获)只是常规成员变量。