On the operator overloading in Delphi

4

The operator overloading in Delphi records is straightforward if a record type does not contain fields which reference heap objects. To illustrate the problem which heap references arise let us consider the following (incorrect) example:

program DelphiDemo;

{$APPTYPE CONSOLE}

uses
  SysUtils;

type
  Adder = record
  private
    FRef: PInteger;
    function GetMemory: Integer;
    procedure SetMemory(AValue: Integer);
  public
    procedure Init(AValue: Integer = 0);
    procedure Done;
    class operator Add(const A, B: Adder): Adder;
    property Memory: Integer read GetMemory write SetMemory;
  end;

{ Adder }

class operator Adder.Add(const A, B: Adder): Adder;
begin
// !!! Memory leak
  New(Result.FRef);
  Result.Memory:= A.Memory + B.Memory;
end;

procedure Adder.Done;
begin
  Dispose(FRef);
end;

function Adder.GetMemory: Integer;
begin
  Result:= FRef^;
end;

procedure Adder.Init(AValue: Integer);
begin
  New(FRef);
  FRef^:= AValue;
end;

procedure Adder.SetMemory(AValue: Integer);
begin
  FRef^:= AValue;
end;

procedure Test;
var
  A, B, C: Adder;

begin
  A.Init(1);
  B.Init(2);
  C.Init();
  C:= A + B;
  Writeln(C.Memory);
  C.Done;
  B.Done;
  A.Done;
end;

begin
  ReportMemoryLeaksOnShutdown:= True;
  try
    Test;
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
  ReadLn;
end.

The line #59 (C:= A + B) in the above program works as follows:

  • A temporary Result record is pushed on stack
  • The temporary record receives the sum A + B
  • The temporary record is assigned (by shallow copying) to the C variable
  • The temporary record is popped from stack

It would work fine if Adder did not reference a heap data; the FRef of the Adder record field makes things complicated. You should always initialize FRef field for every Adder instance but you can’t finalize it for a temporary record that is created on line #59. The only way to solve the memory leak issue in the above code is to comment out the line #58, but it will not work for more complicated right-hand side expressions and it is not a solid approach anyway.

The correct solution involves using a type with automatic memory management instead of a simple pointer. Here is a solution that uses interface:

program DelphiDemo2;

{$APPTYPE CONSOLE}

uses
  SysUtils, Classes;

type
  IAdder = interface
    function GetMemory: Integer;
    procedure SetMemory(AValue: Integer);
  end;

  TAdderRef = class(TInterfacedObject, IAdder)
  private
    FMemory: Integer;
    function GetMemory: Integer;
    procedure SetMemory(AValue: Integer);
  end;

  Adder = record
  private
    FRef: IAdder;
    function GetMemory: Integer;
    procedure SetMemory(AValue: Integer);
  public
    procedure Init(AValue: Integer = 0);
    procedure Done;
    class operator Add(const A, B: Adder): Adder;
    property Memory: Integer read GetMemory write SetMemory;
  end;

{ TAdderRef }

function TAdderRef.GetMemory: Integer;
begin
  Result:= FMemory;
end;

procedure TAdderRef.SetMemory(AValue: Integer);
begin
  FMemory:= AValue;
end;

{ Adder }

class operator Adder.Add(const A, B: Adder): Adder;
begin
  Result.FRef:= TAdderRef.Create;
  Result.Memory:= A.Memory + B.Memory;
end;

procedure Adder.Init(AValue: Integer);
begin
  FRef:= TAdderRef.Create;
  FRef.SetMemory(AValue);
end;

procedure Adder.Done;
begin
  FRef:= nil;
end;

function Adder.GetMemory: Integer;
begin
  Result:= FRef.GetMemory;
end;

procedure Adder.SetMemory(AValue: Integer);
begin
  FRef.SetMemory(AValue);
end;

procedure Test;
var
  A, B, C: Adder;

begin
  A.Init(1);
  B.Init(2);
//  C.Init();
  C:= A + B;
  Writeln(C.Memory);
//  C.Done;
//  B.Done;
//  A.Done;
end;

begin
  ReportMemoryLeaksOnShutdown:= True;
  try
    Test;
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
  ReadLn;
end.

A nice side effect of the above approach is that you need not initialize or finalize the FRef fields manually anymore (though you can still do it). Some lines in the Test procedure above were commented out because they are not needed, but they can be uncommented and the code will remain correct – automatic memory management of interfaces takes care of it.

It is very interesting to know how the problem discussed above is solved in C++. The standard C++ approach is totally different – it involves overloading the assignment operator (a feature which Delphi does not support; Delphi allows to overload only conversionImplicit, Explicit operators) and writing a copy constructor (another concept absent in Delphi object model). I am planning to discuss it later.

On the Pointers and References

6

A Reference is a distinct concept in C/C++ languages. The next code sample (MinGW GCC compiler was used)

#include <iostream>

int main()
  {
    int Value;
    int &ValueRef = Value;

    Value = 2;
    std::cout << "Value: " << Value << "\n";
    std::cout << "ValueRef: " << ValueRef << "\n";

    ValueRef = 3;
    std::cout << "Value: " << Value << "\n";
    std::cout << "ValueRef: " << ValueRef << "\n";
    return 0;
  }

declares ValueRef as a reference to Value variable. The output is
_ref1
Though ValueRef variable is a pointer to Value internally the indirection operator is never used, and the syntax for accessing Value directly or indirectly via ValueRef is the same. ValueRef is also called an alias to Value; the point that ValueRef variable is a pointer internally is just an implementation detail.

Another important thing about C/C++ references is that they are always initialized. The language syntax enforces that you cannot declare a wild reference.

Pascal does not have the same reference concept. The closest concept is a procedure parameter passed by reference:

program ref;

{$APPTYPE CONSOLE}

var
  Value: Integer;

procedure Test(var ValueRef: Integer);
begin
  Writeln('ValueRef: ', ValueRef);
  ValueRef:= 3;
end;

begin
  Value:= 2;
  Writeln('Value: ', Value);
  Test(Value);

  Writeln('Value: ', Value);
  Test(Value);
end.

we can see that

  • ValueRef does not use indirection operator to access a referenced variable;
  • the language syntax enforces that ValueRef is always initialized.

Delphi does not elaborate the reference concept, though there are many built-in types in the language that are ‘transparent pointers’ – objects, interfaces, dynamic arrays, strings. The term reference can be used for example for object variables because the language syntax makes these variables indistinguishable from referenced instances. Instead the term object is usually used, that can mean object reference or object instance, so sometimes you think twice to understand what a particular object does mean.