Search code examples
delphimemory-management

How does Delphi manage memory allocated for Objects created as parameters?


The title might seem a little confusing, but the heart of my question is: What happens to the memory allocated in both of the following scenarios?

Scenario 1:

var
 FHTTPResponse: IHTTPResponse;

...

FHTTPResponse := HTTPClient.Post(RequestURL, TStringStream.Create(APostParams.ToString, TEncoding.UTF8));

The TStringStream object is created in the parameters list of a function (in this case, THTTPClient.Post). What happens with the memory allocated for it, as I created it but (afaik) I'm not able to free it.

Scenario 2:

  ExampleClass = class
    private
      FName, FValue: string;
    public
      constructor Create(AName, AValue: string);

      function AsStringList: TStringList;
  end;

implementation

function ExampleClass.AsStringList: TStringList;
begin
  Result := TStringList.Create;
  Result.Add(FValue);
  Result.Add(FName);
end;

constructor ExampleClass.Create(AName, AValue: string);
begin
  FValue := AValue;
  FName := AName;
end;

When the user calls ExampleClass.AsStringList, my understanding is that I'm not returning a reference to an object, like I would if I had a field FAsStringList, but the object itself.

In this scenario, I'd be passing the responsibility of freeing the TStringList object (which feels just wrong). Does anything else happen underneath this return? Is there a better approach to return the data as different structures, that doesn't imply giving the user the responsibility of freeing the object without them even knowing they have to do so?

TL;DR Is there a better (or a correct) approach to returning data representation as different object types without transferring memory management responsibilities to the user?


Solution

  • Object-type variables/parameters/returns/etc in Delphi are references to objects, not actual objects themselves. Objects reside only in dynamic memory and cannot be passed around by value, only by reference.

    Any call to Create() an object type must have a matching call to Free() (or Destroy()) to free the object from memory, otherwise the object is leaked.

    So, in Scenario 1, the TStringStream is leaked if HTTPClient.Post() does not take ownership of it and free it. And in Scenario 2, the TStringList is leaked if the caller of AsStringList() does not take ownership of the returned list and free it.

    In Scenario 1, let's assume Post() does not take ownership of the stream (seems like a reasonable assumption). The caller should maintain ownership of the stream and free it after Post() returns, eg:

    var
      FHTTPResponse: IHTTPResponse;
      Strm: TStringStream;
    
    ...
    
    Strm := TStringStream.Create(APostParams.ToString, TEncoding.UTF8);
    try
      FHTTPResponse := HTTPClient.Post(RequestURL, Strm);
    finally
      Strm.Free;
    end;
    

    In Scenario 2, the caller must take ownership and free the returned TStringList, eg:

    var
      instance: ExampleClass;
      list: TStringList;
    ...
    list := instance.AsStringList;
    try
      ... 
    finally
      list.Free;
    end;
    

    If you don't want to transfer ownership of the TStringList to the caller, then ExampleClass must maintain ownership of it internally, eg:

    type
      ExampleClass = class
      private
        FName, FValue: string;
        FList: TStringList;
      public
        constructor Create(AName, AValue: string);
        destructor Destroy; override;
    
        function AsStringList: TStringList;
      end;
    
    implementation
    
    constructor ExampleClass.Create(AName, AValue: string);
    begin
      FValue := AValue;
      FName := AName;
      FList := nil;
    end;
    
    destructor ExampleClass.Destroy;
    begin
      FList.Free;
      inherited Destroy;
    end;
    
    function ExampleClass.AsStringList: TStringList;
    begin
      if FList = nil then
        FList := TStringList.Create
      else
        FList.Clear;
      FList.Add(FValue);
      FList.Add(FName);
      Result := FList;
    end;
    

    Otherwise, the caller would need to create its own list and pass it to the class to fill in, eg:

    type
      ExampleClass = class
      private
        ...
      public
        ...
        procedure AsStringList(AList: TStrings);
      end;
    
    implementation
    
    procedure ExampleClass.AsStringList(AList: TStrings);
    begin
      AList.Add(FValue);
      AList.Add(FName);
    end;
    
    var
      instance: ExampleClass;
      list: TStringList;
    ...
    list := TStringList.Create;
    try
      ...
      instance.AsStringList(list);
      ...
    finally
      list.Free;
    end;