Thursday, January 08, 2015

Implementing Destructor for a Record

Smart records in Object Pascal are very nice, but they have a stupid limitation – you cannot implement a destructor for a record.

image

A solution for that is quite simple and can be found all over the internet – add an interface to this record and implement the cleanup in this interface. To make it even simpler, you can use the IGpAutoExecute interface from the completely free GpStuff unit.

You have to find a good place to create such a guard interface. You can, for example, do this in a constructor. (Where we hit another limitation – you cannot have a parameter-less constructor on a record.)

type
TRec = record
private
FDestructor: IGpAutoExecute;
public
constructor Create(data: boolean);
end;

constructor TRec.Create(data: boolean);
begin
FDestructor := AutoExecute(
procedure
begin
// do the cleanup here
end);
end
;

This, however, creates another problem. In such “destructor” you are unable to access the record itself.


image


A good question to ask yourself at this point is – what do you want to do in the destructor? If you merely want to destroy some object, then you can just convert this object into some kind of a smart pointer or add a specific guard for that object.


Another solution is to change the game and instead of capturing self, capture a local variable. My logging example from above can be changed to an equivalent, but working code.

constructor TRec.Create(data: integer);
var
s: string;
begin
FData := data;
Writeln('Record [', FData, '] created');
s := Format('Record [%d] destroyed', [FData]);
FDestructor := AutoExecute(
procedure
begin
Writeln(s);
end);
end
;

All such code must be used with care, and I’ll show you why. Below is a functional test program.

program DestructorTest;

{$APPTYPE CONSOLE}

{$R *.res}

uses
System.SysUtils,
GpStuff;

type
TRec = record
private
FDestructor: IGpAutoExecute;
FData: integer;
public
constructor Create(data: integer);
end;

{ TRec }

constructor TRec.Create(data: integer);
var
s: string;
begin
FData := data;
Writeln('Record [', FData, '] created');
s := Format('Record [%d] destroyed', [FData]);
FDestructor := AutoExecute(
procedure
begin
Writeln(s);
end);
end;

procedure Test;
var
rec1, rec2: TRec;
begin
rec1 := TRec.Create(0);
Write('1>'); Readln;
rec2 := rec1;
Write('2>'); Readln;
rec2 := TRec.Create(1);
Write('3>'); Readln;
end
;

begin
try
Test;
Write('4>'); Readln;
except
on E: Exception do
Writeln(E.ClassName, ': ', E.Message);
end;
end
.

Its output:


image


First, one record is created. The code waits for Enter.


Next, record is copied to another record (rec2 := rec1). As you can see from the output, nothing happens at that point. Compiler just assigns data from one record to another and increments the reference count along the way.


The code then waits for the second Enter and creates another instance of the TRec. Although this instance overwrites the rec2 variable, previous rec2 is not destroyed. Compiler merely decrements refcount of the previous IGpAutoExecute but rec1 still owns it and the object implementing the interface is not destroyed.


After the third Enter, the code exits the Test procedure. At that point both records go out of scope, refcount for each FDestructor is decremented and both “destructor” blocks are called.



If we add a TList to the record, we’ll see that exactly the same thing happens – TList is neither created nor destroyed when a record is assigned to another.

program DestructorTest;

{$APPTYPE CONSOLE}

{$R *.res}

uses
System.SysUtils,
System.Classes,
GpStuff;

type
TMyList = class
public
constructor Create;
destructor Destroy; override;
end;

TRec = record
private
FDestructor: IGpAutoExecute;
FData: integer;
FList: TMyList;
public
constructor Create(data: integer);
end;

{ TRec }

constructor TRec.Create(data: integer);
var
list: TMyList;
s: string;
begin
FData := data;
FList := TMyList.Create;
Writeln('Record [', FData, '] created');

s := Format('Record [%d] destroyed', [FData]);
list := FList;
FDestructor := AutoExecute(
procedure
begin
list.Free;
Writeln(s);
end);
end;

procedure Test;
var
rec1, rec2: TRec;
begin
rec1 := TRec.Create(0);
Write('1>'); Readln;
rec2 := rec1;
Write('2>'); Readln;
rec2 := TRec.Create(1);
Write('3>'); Readln;
end;

{ TMyList }

constructor TMyList.Create;
begin
Writeln('List created');
end;

destructor TMyList.Destroy;
begin
Writeln('List destroyed');
end;

begin
try
Test;
Write('4>'); Readln;
except
on E: Exception do
Writeln(E.ClassName, ': ', E.Message);
end;
end
.

image


This is something that you should always keep in mind when working with smart records – there is no way to intercept the assignment. In most cases you can design your solution so that this doesn’t represent a problem.


[It just crossed my mind that there may be a way to detect such assignment by overriding the _AddRef on an interface. I’ll have to look into that …]

6 comments:

  1. ? What will happen if I run the next code:

    procedure Test;
    var
    rec1, rec2: TRec;
    begin
    rec1 := TRec.Create(0);
    Write('1>'); Readln;
    rec2 := rec1;
    Write('2>'); Readln;
    end;

    ReplyDelete
    Replies
    1. Start the Delphi and find out?

      Delete
    2. I don't have your libs, I just reading the article. I am asking do the above code corrupts memory with probable AV or not.

      Delete
    3. a) Downloading one needed unit is simple and nothing beats hands-on experience when you want to learn something.

      b) OK, OK:

      List created
      Record [0] created
      1>
      2>
      List destroyed
      Record [0] destroyed
      4>

      No memory corruption and I don't see any reason for it.

      Delete
    4. Ok, thanks. Just wanted to be sure that "unintercepted" assignment does nothing wrong here; still I believe a user of this "smart pointer" should understand how refcounting works under the hood to use the "smart pointer" correctly.

      Delete
    5. I can only agree with that. You should understand refcounting if you want to use interfaces. Period.

      Delete