Thursday, February 28, 2013

When Does “Execute on Destroy” Fail

Yesterday I wrote a post about executing some code automatically when a method ends. Today I’ll show you why this approach must be used with care.

Let’s start with a working example. Create Delphi VCL application, throw a TListBox and a TButton on the form and write this simple Button1.OnClick event. [Pardon my ugly formatting; I was trying to save some space.]

procedure TForm8.Button1Click(Sender: TObject);
begin
  AutoExecute(procedure begin
    ListBox1.Items.Add('AutoExecute 1');
  end);
  AutoExecute(procedure begin
    ListBox1.Items.Add('AutoExecute 2');
  end);
  ListBox1.Items.Add('Will end now …');
end;

The output from this program will be partially expected and (maybe) partially surprising.

image

As we see the code is indeed executed at the end of the Button1Click method, but interfaces are destroyed in the reverse order of creation (which usually doesn’t cause any problems).

The code created by the compiler is equivalent to this program.

procedure TForm8.Button1Click(Sender: TObject);
var
  temp1, temp2: IGpAutoExecute;
begin
  temp1 := nil; temp2 := nil;
  try
    temp1 := AutoExecute(procedure begin
      ListBox1.Items.Add('AutoExecute 1');
    end);
    temp2 := AutoExecute(procedure begin
      ListBox1.Items.Add('AutoExecute 2');
    end);
    ListBox1.Items.Add('Will end now …');
  finally
    temp2 := nil;
    temp1 := nil;
  end;
end;

Next, I’ll show an example of a bad pattern usage.

procedure TForm8.Button1Click(Sender: TObject);
var
  i: integer;
begin
  for i := 1 to 5 do
    AutoExecute(procedure begin
      ListBox1.Items.Add('AutoExecute');
    end);
  ListBox1.Items.Add('Will end now …');
end;

The output may surprise you.

image

Only one AutoExecute code segment is executed at the end of the method! Why?

Let’s see what the compiler really does in this case.

procedure TForm8.Button1Click(Sender: TObject);
var
  i: integer;
  temp: IGpAutoExecute;
begin
  temp := nil;
  try
    for i := 1 to 5 do
      temp := AutoExecute(procedure begin
        ListBox1.Items.Add('AutoExecute');
      end);
    ListBox1.Items.Add('Will end now …');
  finally
    temp := nil;
  end;
end;

Compiler only creates one temporary variable and it is reused inside the for statement. Every additional assignment clears the previous content and causes destructor to be called. Only the last instance (created when i = 5) is destroyed when method exits.

There’s additional problem with running anonymous methods from a for loop. Because the values are captured by a reference, not by a value (IOW, when you call AutoExecute in the code above, it gets an address of the for loop variable i, not its content), the value when destructor is called does not equal the value at the moment when AutoExecute is called.

procedure TForm8.Button1Click(Sender: TObject);
var
  i: integer;
begin
  for i := 1 to 5 do
    AutoExecute(
      procedure
      begin
        ListBox1.Items.Add('AutoExecute #' + IntToStr(i));
      end);
  ListBox1.Items.Add('Will end now …');
end;

image

If you need to create anonymous methods inside a loop, use the following trick.

function TForm8.AutoLog(value: integer): TProc;
begin
  Result := procedure begin
    ListBox1.Items.Add('AutoExecute #' + IntToStr(value));
  end;
end;

procedure TForm8.Button1Click(Sender: TObject);
var
  i: integer;
begin
  for i := 1 to 5 do
    AutoExecute(AutoLog(i));
  ListBox1.Items.Add('Will end now …');
end;

By moving anonymous method creation into a separate function we are ensuring that correct value of the parameter is captured. Of course, the code is still called at the wrong time but at least the values are correct …

image

4 comments:

  1. Binis17:14

    Heh this reminds me why using

    procedure TMyThread.Execute;
    ....
    (myIntf as IMyInterface).DoSoemthing;
    ....
    while not Terminated do
    ....
    end;

    is bad. It took me a while to figure why my classes was leaking.

    ReplyDelete
  2. Anonymous12:03

    I don't get it, if you declare a method, why use anonymous method?

    ReplyDelete
    Replies
    1. I don't understand your question.

      Delete
  3. This remember-me that you need to know when your destructors run. https://blogs.msdn.com/b/oldnewthing/archive/2004/05/20/135841.aspx?Redirected=true

    Must admit that I'm not a fan of this kind of code...

    ReplyDelete