Search code examples
delphigenericsdelphi-xe2

Delphi - smart pointers and generics TList


I have an implementation of smart pointers, and I've tried to implement it on a generic TList.

program Project2;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.SysUtils,
  System.Generics.Collections;


type
  ISmartPointer<T> = reference to function: T;

  TSmartPointer<T: class, constructor> = class(TInterfacedObject, ISmartPointer<T>)
  private
    FValue: T;
    FName: string;
  public
    constructor Create; overload;
    constructor Create(AValue: T); overload;
    destructor Destroy; override;
    function Invoke: T;

    property Name: string read FName write FName;
  end;

  TTest = class
    FString: String;
  public
   property MyStrign: String read FString write FString;
  end;

{ TSmartPointer<T> }

constructor TSmartPointer<T>.Create;
begin
  inherited;

  FValue := T.Create;
end;

constructor TSmartPointer<T>.Create(AValue: T);
begin
  inherited Create;
  if AValue = nil then
    FValue := T.Create
  else
    FValue := AValue;
end;

destructor TSmartPointer<T>.Destroy;
begin
  if Assigned(FValue) then
    FValue.Free;

  inherited;
end;

function TSmartPointer<T>.Invoke: T;
begin
  Result := FValue;
end;

function TestSMP():ISmartPointer<TList<TTest>>;
var lTTest: ISmartPointer<TTest>;
    i: Integer;
begin
 Result := TSmartPointer<TList<TTest>>.Create();

 for I := 0 to 5 do
  begin
    lTTest := TSmartPointer<TTest>.Create();
    lTTest.FString := IntToStr(i);

    Result().Add(lTTest);
  end;
end;

var Testlist:ISmartPointer<TList<TTest>>;
    i: Integer;

begin
  try
   Testlist := TestSMP();

   for I := 0 to 5 do
    Writeln(Testlist[i].FString);
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
 Writeln('finished');
 Readln;
end.

The problem is that I can not access the elements from the list and I have no clue where the issue is.


Solution

  • function TestSMP(): ISmartPointer<TList<TTest>>;
    var 
       lTTest: ISmartPointer<TTest>;
       i: Integer;
    begin
       Result := TSmartPointer<TList<TTest>>.Create();
    
       for I := 0 to 5 do
       begin
          lTTest := TSmartPointer<TTest>.Create();
          lTTest.FString := IntToStr(i);
    
          Result().Add(lTTest);
       end;
    end;
    

    The lTTest interface variable is the only thing that is keeping the TTest instances alive. Each time around the loop, when you assign to lTTest, the previous TTest instance is destroyed. When the function exits, the final TTest instance, the one containing '5' is destroyed. All the instances you lovingly created are now dead.

    You can observe this happening by putting a breakpoint inside TSmartPointer<T>.Destroy and looking at the call stack. One of the consequences of this is that your code is actually referring to the TTest instances after they have been destroyed. By chance, neither you nor I are observing runtime errors, although it is clearly wrong to do this.

    The key point here is that once you start managing the lifetime using smart pointers, you have to do so exclusively. In for a penny, in for a pound. Which pretty much pushes you to replace

    ISmartPointer<TList<TTest>>
    

    with

    ISmartPointer<TList<ISmartPointer<TTest>>>
    

    That's because you have started managing the TTest instance lifetimes by wrapping with a smart point. Once you've started doing that, you've got to do so consistently.

    Consider this variant of your program:

    {$APPTYPE CONSOLE}
    
    uses
      System.SysUtils,
      System.Generics.Collections;
    
    type
      ISmartPointer<T> = reference to function: T;
    
      TSmartPointer<T: class, constructor> = class(TInterfacedObject,
        ISmartPointer<T>)
      private
        FValue: T;
      public
        constructor Create;
        destructor Destroy; override;
        function Invoke: T;
      end;
    
      TTest = class
        FString: string;
      end;
    
    constructor TSmartPointer<T>.Create;
    begin
      inherited;
      FValue := T.Create;
    end;
    
    destructor TSmartPointer<T>.Destroy;
    begin
      FValue.Free;
      inherited;
    end;
    
    function TSmartPointer<T>.Invoke: T;
    begin
      Result := FValue;
    end;
    
    function TestSMP(): ISmartPointer<TList<ISmartPointer<TTest>>>;
    var
      lTTest: ISmartPointer<TTest>;
      i: Integer;
    begin
      Result := TSmartPointer<TList<ISmartPointer<TTest>>>.Create();
    
      for i := 0 to 5 do
      begin
        lTTest := TSmartPointer<TTest>.Create();
        lTTest.FString := IntToStr(i);
        Result().Add(lTTest);
      end;
    end;
    
    var
      i: Integer;
      Testlist: ISmartPointer<TList<ISmartPointer<TTest>>>;
    
    begin
      Testlist := TestSMP();
      for i := 0 to 5 do
        Writeln(Testlist[i]().FString);
      Writeln('finished');
      Readln;
    end.
    

    Output

    0
    1
    2
    3
    4
    5
    finished
    

    I don't think I like this idea of ISmartPointer<TList<ISmartPointer<TTest>>> very much. Honestly, I've never been convinced by the effectiveness of smart pointers in Delphi.