Thread overview
Lambda capture by value
Feb 24, 2020
JN
Feb 24, 2020
Adam D. Ruppe
Feb 24, 2020
JN
Feb 24, 2020
Elronnd
Feb 24, 2020
kinke
Feb 24, 2020
H. S. Teoh
February 24, 2020
import std.range;
import std.stdio;

alias NumberPrinter = void delegate();

NumberPrinter[int] printers;

void main()
{
    foreach (i; iota(5))
    {
        printers[i] = () { write(i); };
    }

    foreach (i; iota(5))
    {
        printers[i]();
    }
}

This prints 4 4 4 4 4.

How to make it so that it prints 0 1 2 3 4? Is it possible without changing the delegate definition to void delegate(int)?
February 24, 2020
On Monday, 24 February 2020 at 19:50:23 UTC, JN wrote:
>     foreach (i; iota(5))
>     {
>         printers[i] = () { write(i); };

I know it looks silly but if you make that:

         printers[i] = (int i) { return () { write(i); }; }(i);

it will do what you want.

This is something that used to be common in javascript, write a little function that passes the capture-by-value args and returns the lambda you actually want and call it immediately.

That extra layer causes the compiler to create a new copy of the capture variables snapshotted in time.
February 24, 2020
On Monday, 24 February 2020 at 20:00:20 UTC, Adam D. Ruppe wrote:
> On Monday, 24 February 2020 at 19:50:23 UTC, JN wrote:
>>     foreach (i; iota(5))
>>     {
>>         printers[i] = () { write(i); };
>
> I know it looks silly but if you make that:
>
>          printers[i] = (int i) { return () { write(i); }; }(i);
>
> it will do what you want.
>
> This is something that used to be common in javascript, write a little function that passes the capture-by-value args and returns the lambda you actually want and call it immediately.
>
> That extra layer causes the compiler to create a new copy of the capture variables snapshotted in time.

D'oh! I am actually familiar with the pattern from Javascript, used it many times, but somehow got it mixed up with something else and couldn't make it work.

Thanks.
February 24, 2020
On Monday, 24 February 2020 at 19:50:23 UTC, JN wrote:
>     foreach (i; iota(5))
>     {
>         printers[i] = () { write(i); };
>     }

This allocates 1 closure and generates 1 lambda, so all printers are identical delegates. You could use a static foreach:

NumberPrinter[] printers;
static foreach (i; 0..5)
    printers ~= () { write(i); };
foreach (d; printers)
        d();
February 24, 2020
On Mon, Feb 24, 2020 at 07:50:23PM +0000, JN via Digitalmars-d-learn wrote:
> import std.range;
> import std.stdio;
> 
> alias NumberPrinter = void delegate();
> 
> NumberPrinter[int] printers;
> 
> void main()
> {
>     foreach (i; iota(5))
>     {
>         printers[i] = () { write(i); };
>     }
> 
>     foreach (i; iota(5))
>     {
>         printers[i]();
>     }
> }
> 
> This prints 4 4 4 4 4.
> 
> How to make it so that it prints 0 1 2 3 4? Is it possible without changing the delegate definition to void delegate(int)?

The cause of the problem is that 'i' in the first foreach loop is *reused* across loop iterations, so all 5 lambdas are actually closing over the same variable, which gets its value replaced by the next iteration.

To fix this, copy the value of 'i' to a local variable inside the loop body, then the lambda will correctly capture a unique per-iteration instance of the variable.  Like this:

    foreach (i; iota(5))
    {
	auto _i = i;
        printers[i] = () { write(_i); };
    }


T

-- 
Curiosity kills the cat. Moral: don't be the cat.
February 24, 2020
>>         printers[i] = () { write(i); };
>
> I know it looks silly but if you make that:
>
>          printers[i] = (int i) { return () { write(i); }; }(i);
>
> it will do what you want.

Or, the slightly prettier (imo)

          printers[i] = ((i) => () => write(i))(i);

Or, if you need to force it to return void:

          printers[i] = ((i) => {write(i);})(i);
February 24, 2020
On 2/24/20 3:32 PM, H. S. Teoh wrote:

> To fix this, copy the value of 'i' to a local variable inside the loop
> body, then the lambda will correctly capture a unique per-iteration
> instance of the variable.  Like this:
> 
>      foreach (i; iota(5))
>      {
> 	auto _i = i;
>          printers[i] = () { write(_i); };
>      }

Nope. It doesn't ;) Because actually, _i is reused for the loop iteration.

You are thinking that the capture happens on the delegate creation. In essence, it just sets a flag to the compiler that the enclosing function must be placed on the heap.

Adam's method works because the nested call's stack frame is captured when you return that delegate.

In other words, it looks like this:

void main()
{
   static struct __mainStackFrame
   {
       int i;
       int _i;
   }

   __mainStackFrame *frame = new __mainStackFrame; // here's where the capture happens
   with(*frame)
   {
      // main's function body, with all variables already being declared
   }
}

-Steve