I'm trying to understand how closures are actually sent for lambda expressions calls:
#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;
}
When I inspect the generated machine code, I see the captured entities are passed on stack:
$ 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>
Do I have any control of how the compiler manages this closure move?
You have to conceptually separate the initialization of the object of closure type, and the function call. A lambda expression has a corresponding closure type. In your case, myLovelyLambda
would translate into something like this:
// 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]);
}
};
};
Note that the order of argc
, argv
, and A
in the closure type is unspecified according to [expr.prim.lambda] p10:
For each entity captured by copy, an unnamed non-static data member is declared in the closure type. The declaration order of these members is unspecified.
Initialization and calling is then transformed like this:
// const auto myLovelyLambda = [=]() { ... };
const auto myLovelyLambda = __lambda{argc, argv, A};
myLovelyLambda();
Do I have any control of how the compiler manages this closure move?
Not really. You can't have volatile
captures in a lambda, so the compiler is free to transform and reorder the initialization of the captures in the object significantly. It can also optimize away the lambda completely through inlining, so that the assembly is indistinguishable form having your for
loop directly main
.
It is also unspecified in what order lambda captures are initialized within the closure object, and in what order they are destroyed. See C++11: In what order are lambda captures destructed?. In the end, you can just let the compiler figure it out. In your example, you don't need tight control over captures order.