Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Anonymous methods - variable capture versus value capture

Below is a SSCCE based on an example in the Anonymous Methods section of Part 1 of Chris Rolliston's excellent Delphi XE2 Foundations book, about the idea of variable capture (any errors in it are entirely down to me).

It works exactly as I'd expect, logging 666, 667, 668, 669 on successive clicks of the BtnInvoke button. In particular it nicely illustrates how the captured version of the local variable I persists long after btnSetUpClick exits.

So far so good. The problem I'm asking about isn't with this code per se but with what's said in Allen Bauer's blog here:

http://blogs.embarcadero.com/abauer/2008/10/15/38876

Now, I know better than to argue with the boss, so I am sure I'm missing the point of the distinction he draws between variable capture and value capture. To my simple way of looking at it, my CR-based example captures the value of I by capturing I as a variable.

So, my question is, what is the distinction Mr Bauer is trying to draw, exactly?

(Btw, despite watching the Delphi section of SO daily for 9+ months, I'm still not entirely clear if this q is on-topic. If not, my apologies and I'll take it down.)

type
  TAnonProc = reference to procedure;

var
  P1,
  P2 : TAnonProc;

procedure TForm2.Log(Msg : String);
begin
  Memo1.Lines.Add(Msg);
end;

procedure TForm2.btnSetUpClick(Sender: TObject);
var
  I : Integer;
begin
  I := 41;
  P1 := procedure
    begin
      Inc(I);
      Log(IntToStr(I));
    end;

  I := 665;
  P2 := procedure
    begin
      Inc(I);
      Log(IntToStr(I));
    end;
end;

procedure TForm2.btnInvokeClick(Sender: TObject);
begin
  Assert(Assigned(P1));
  Assert(Assigned(P2));

  P1;
  P2;
end;
like image 807
MartynA Avatar asked Jun 14 '14 19:06

MartynA


2 Answers

Variable capture vs value capture is simple enough. Let us suppose that two anonymous methods capture the same variable. Like this:

Type
  TMyProc = reference to procedure;
var
  i: Integer;
  P1, P2: TMyProc;
....
i := 0;
P1 := procedure begin Writeln(i); inc(i); end;
P2 := procedure begin Writeln(i); inc(i); end;
P1();
P2();
Writeln(i);

There is a single variable that is captured by both methods. The output is:

0
1
2

This is capture of a variable. If the value was captured, which it isn't, one might imagine that the two methods would have separate variables that both started with value 0. And both functions might output 0.

In your example, you are supposed to imagine that P1 captures the value 41, and P2 captures the value 665. But that does not happen. There is exactly one variable. It is shared between the procedure that declares it, and the anonymous methods that capture it. It lives as long as all parties that share it live. Modifications to the variable made by one party are seen by all others because there is exactly one variable.


So, it is not possible to capture a value. To get behaviour that feels like that you need to copy a value to a new variable, and capture that new variable. That can be done with, for instance, a parameter.

function CaptureCopy(Value: Integer): TMyProc;
begin
  Result := procedure begin Writeln(Value); end;
end;

...
P3 := CaptureCopy(i);

This will copy the value of i into a new variable, the procedure parameter, and capture that. Subsequent changes to i have no influence on P3 because the captured variable is local to P3.

like image 157
David Heffernan Avatar answered Oct 21 '22 00:10

David Heffernan


Let us clarify the things a bit; internally any data captured by an anonymous method is a field of a hidden object instance and should be called variable as such; but there can be different cases of capturing a variable.

Consider the sample code:

type
  TMyProc = reference to procedure;

function CaptureValue(Value: Integer): TMyProc;
begin
  Result := procedure begin Inc(Value); Writeln(Value); end;
end;

procedure Test1;
var
  Proc1: TMyProc;
  I: Integer;

begin
  I:= 32;
  Proc1:= CaptureValue(I);
  Proc1();
  Writeln(I);     // 32
end;

procedure Test2;
var
  Proc2: TMyProc;
  I: Integer;

begin
  I:= 32;
  Proc2:= procedure begin Inc(I); Writeln(I); end;
  Proc2();
  Writeln(I);    // 33
end;

You can see that logically the Proc1 (in Test1) captures a value of I while the Proc2 (in Test2) captures a reference to I.

If you look under the hood you'll notice that the variable I in Test1 is a plain local stack-based variable while Proc1 accesses a field of a hidden object instance (using reference to the instance and offset of the field); we have 2 different variables (one on stack and the other on heap).

Test2 has no stack-based I variable at all, only a field of a hidden object instance; both Test2 and Proc2 access the same variable by reference to the instance (and the field's offset); we have a single heap variable.

like image 34
kludg Avatar answered Oct 20 '22 23:10

kludg