2

I have a record that contains what I believe is a pointer to a reference counted object. I would expect that if I create the reference counted object within the record that when the record goes out of scope the reference count of the object would fall to zero, and the object would be destroyed. But this does not seem to be that case. Here is sample minimum code. My form happens to have some panels and a memo, but only the TButton (and specifically Button1Click) is important.

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.ExtCtrls;

type
  TUserData = class( TInterfacedObject )
  public
    AData : integer;

    constructor Create;
    destructor Destroy; override;
  end;

  TTestRec = Record
    AField : integer;
    UserData : TUserData;
  End;

  TForm4 = class(TForm)
    Panel1: TPanel;
    Panel2: TPanel;
    Panel3: TPanel;
    Memo1: TMemo;
    Button1: TButton;
    procedure FormShow(Sender: TObject);
    procedure Button1Click(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form4: TForm4;

implementation

{$R *.dfm}

procedure TForm4.Button1Click(Sender: TObject);
var
  iRec : TTestRec;
begin
   iRec.UserData := TUserData.Create;
   // stop too much optimisation
   Button1.Caption := IntToStr( iRec.UserData.AData );
end; // I would expect TTestRec and hence TTestRec.UserData to go out of scope here

procedure TForm4.FormShow(Sender: TObject);
begin
  // show leaks on exit
  ReportMemoryLeaksOnShutdown := TRUE;
end;

{ TUserData }

constructor TUserData.Create;
begin
  inherited Create;
  AData := 4;
end;

destructor TUserData.Destroy;
begin

  inherited;
end;

end.

I confess I don't really understand how reference counting works in detail, although I do understand the principle. What am I missing? Am I expecting too much and if so, is there any way to avoid memory leaks, not in this specific case (where obviously I could destroy UserData on exit) but in general, since records do not support destructors.

Dsm
  • 5,870
  • 20
  • 24
  • Take a look at http://stackoverflow.com/questions/3920470/why-arent-addref-and-release-called-on-my-delphi-object/, it should help you understand what's going on better. – Ken Bourassa Nov 21 '16 at 17:34

1 Answers1

6

Automatic reference counting is performed through interface variables. You don't have any. Instead of a variable of type TUserData you need a variable that is an interface.

You could use IInterface here but that would be a little useless. So you should define an interface that exposes the public functionality you need the object to support and then have your class implement that interface.

This program demonstrates what I mean:

type
  IUserData = interface
    ['{BA2B50F5-9151-4F84-94C8-6043464EC059}']
    function GetData: Integer;
    procedure SetData(Value: Integer);
    property Data: Integer read GetData write SetData;
  end;

  TUserData = class(TInterfacedObject, IUserData)
  private
    FData: Integer;
    function GetData: Integer;
    procedure SetData(Value: Integer);
  end;

function TUserData.GetData: Integer;
begin
  Result := FData;
end;

procedure TUserData.SetData(Value: Integer);
begin
  FData := Value;
end;

type
  TTestRec = record
    UserData: IUserData;
  end;

procedure Main;
var
  iRec: TTestRec;
begin
  iRec.UserData := TUserData.Create;
end;

begin
  Main;
  ReportMemoryLeaksOnShutdown := True;
end.

This program does not leak. Change the variable declaration in the record type to UserData: TUserData and the leak returns.

David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
  • Thank you so much for this explanation. The fog is (slowly) starting to clear. Having read the TInterfacedObject documentation, and it saying it introduced *support* for reference counting, a had assumed (wrongly) that I was creating a reference counted object. – Dsm Nov 21 '16 at 19:32
  • 1
    You are creating a reference counted object. But you need something to count references. And that happens through interface variables. – David Heffernan Nov 21 '16 at 19:34
  • Actually, for me the lightbulb moment was the last paragraph where you commented that the declaration had to be an interface, and defining it as a class brought the leak back. – Dsm Nov 22 '16 at 08:05