On 8/4/17 12:57 PM, bitwise wrote:
I'm confused about how D's lambda capture actually works, and can't find
any clear specification on the issue. I've read the comments on the bug
about what's described below, but I'm still confused. The conversation
there dropped off in 2016, and the issue hasn't been fixed, despite high
bug priority and plenty of votes.
Consider this code:
void foo() {
void delegate()[] funs;
foreach(i; 0..5)
funs ~= (){ writeln(i); };
foreach(fun; funs)
fun();
}
void bar() {
void delegate()[] funs;
foreach(i; 0..5)
{
int j = i;
funs ~= (){ writeln(j); };
}
foreach(fun; funs)
fun();
}
void delegate() baz() {
int i = 1234;
return (){ writeln(i); };
}
void overwrite() {
int i = 5;
writeln(i);
}
int main(string[] argv)
{
foo();
bar();
auto fn = baz();
overwrite();
fn();
return 0;
}
First, I run `foo`. The output is "4 4 4 4 4".
So I guess `i` is captured by reference, and the second loop in `foo`
works because the stack hasn't unwound, and `i` hasn't been overwritten,
and `i` contains the last value that was assigned to it.
Next I run `bar`. I get the same output of "4 4 4 4 4". While this hack
works in C#, I suppose it's reasonable to assume the D compiler would
just reuse stack space for `j`, and that the C# compiler has some
special logic built in to handle this.
Now, I test my conclusions above, and run `baz`, `overwrite` and `fn`.
The result? total confusion.
The output is "5" then "1234". So if the lambdas are referencing the
stack, why wasn't 1234 overwritten?
Take a simple C++ program for example:
int* foo() {
int i = 1234;
return &i;
}
void overwrite() {
int i = 5;
printf("%d\n", i);
}
int main()
{
auto a = foo();
overwrite();
printf("%d\n", *a);
return 0;
}
This outputs "5" and "5" which is exactly what I expect, because I'm
overwriting the stack space where the first `i` was stored with "5".
So now, I'm thinking.... D must be storing these captures on the heap
then..right? So why would I get "4 4 4 4 4" instead of "0 1 2 3 4" for
`foo` and `bar`?
This makes absolutely no sense at all.
Because the stack frame of foo or bar or baz is stored on the heap
BEFORE the function is entered. The compiler determines that the stack
frame will need to be captured, so it captures it on function entry, not
when the delegate is taken. Then the variable location is reused for the
loop, and all delegates point at the same stack frame.
This is necessary for cases where the delegate may affect the frame data
during the function call. For instance:
void foo()
{
int i;
auto dg = { ++i;};
dg();
dg();
assert(i == 2);
}
What is needed is to allocate one frame per scope, and have the delegate
point at the right ones.
Note, the C++ behavior uses dangling stack pointers, and not something
we want to support in D.
-Steve