Friday, 1 September 2017

Smart pointers

Introduction

Today, I had a discussion on StackOverflow (I hope no one deleted it yet) on the usefulness of smart pointers instead of try...finally.

My arguments are as follows:

1. try...finally decreases the readability of a function, especially if they are nested, e.g. when you instantiate a TStream and then a TStringList. Then you would need two nested try...finally constructs, thoroughly decreasing readability.

2. The code that destroys (frees) the object can be several lines removed from the place where it is created. Smart pointers allow you to take care of lifetime management right where you create the object.

3. Even if you are very good at manual memory management, taking care of it is still a chore, and it is always nice to have something, like ARC, or smart pointers, that can take that away from you. It greatly reduces the number of situations where you can make mistakes.

4. No matter how good you are, you can always forget to call Free, or call LongNamedObjectA.Free when you actually meant LongNamedObjectAB.Free. Such errors are hard to find, especially if they are several lines away from the actual creation spot. Using smartpointers, you don't have to repeat yourself this way, so you can't make such mistakes.

So if people tell you that only lazy or incompetent people would use smart pointers, or that people won't be able to understand your code if you use them, don't believe it. They greatly reduce the chance to make mistakes, even if you are competent at manual memory and lifetime management. They remove a lot of clutter and make your code more readable and let you concentrate on what you want to do, not on lifetime management anymore.

Implementation

I have seen quite a few smart pointer implementations. The nice thing about them is that they take care of automatically destroying the "guarded" object, so you don't have to take care of that yourself. So instead of something like:

var
  X: TMyObject;
begin
  X := TMyObject.Create(17);
  try
    // Several lines of code using X
  finally
    X.Free;
  end;
end;

Now should be able to do something like:

var
  X: TMyObject;
begin
  X := SmartPointer<TMyObject>(TMyObject.Create(17));
  // Several lines of code using X
end;

And at the end of the function, X is freed automatically.

Awkward to use

Unlike what I want to have, most implementations I have seen require you to declare the smart pointers as extra variables:

var
  X: TMyObject;
  SP: SmartPointer<TMyObject>;
begin
  X := SmartPointer<TMyObject>(SP, TMyObject.Create(17));
  // Several lines of code using X
end;

This is so for the ObjectHandle by Barry Kelly, as well as the ISafeGuard types in the JCL. But I never liked that. It makes you repeat yourself needlessly and makes the code less readable than it could be, taking away one of the advantages smart pointers provide, in my opinion.

Trick

So I employ a few tricks. The first one is that if a function or constructor returns a record, and this record is never assigned to a variable, the compiler still puts an anonymous record on the stack. That is because, for any record that is larger than a register, a record that is returned is actually passed as a var parameter. So the following:

function Bla(I, J: Integer): TMyRecord;

is actually compiled as:

procedure Bla(I, J: Integer; var Result: TMyRecord);

But that means that an actual record must be passed. If, in your code that uses the function, you don't assign the result of the function, the compiler creates an anonymous record on the stack and passes that, so inside your function you can assign values to the fields of Result.

Note that the use of the anonymous record mechanism is safe. It is also employed for intermediate results of class operators like + or *. If I use my BigIntegers, or Delphi's sample TComplex, then D := A + B * C; actually produces the code AnonymousIntermediate := op_Multiply(B, C); D := op_Add(A, AnonymousIntermediate); It will not change anytime soon.

Here comes the second trick: I want to return a TMyObject, and not a smart pointer record, so I can assign the result directly to X. For this, I (ab)use the Implicit operators you can define for a record. I define a

class operator Implicit(A: TMyObject): SmartPointer<TMyObject>;
and a
class operator Implicit(A: SmartPointer<TMyObject>): TMyObject;
So if I do the following:

X := SmartPointer<TMyObject>(TMyObject.Create(17));

then first, the first Implicit operator returns a record (a smart pointer). This is an anonymous record. But since X is a TMyObject, subsequently the other Implicit operator converts the anonymous record into a TMyObject again. How this "conversion" (actually just some passing around of a reference) is done can be seen in the code a bit further on.

So now I don't have to explicitly declare any smart pointers and can return and assign the created object in one fell swoop. The only thing to take care of is making this generic. That is what I have done in the following simple unit.

Many years ago, I implemented the original smart pointers in the JCL and called them Guards, and I want to remain faithful to that name. But since they are a little "smarter" and easier to use then those early implementations (which did not have overloaded operators or records with methods yet), I called them SmartGuards instead.

The unit

unit Velthuis.SmartGuards;

interface

type
  IGuard = interface
  ['{CE522D5D-41DE-4C6F-BC84-912C2AEF66B3}']
  end;

  TGuard = class(TInterfacedObject, IGuard)
  private
    FObject: TObject;
  public
    constructor Create(AObject: TObject);
    destructor Destroy; override;
  end;

  SmartGuard<T: class> = record
  private
    FGuard: IGuard;
    FGuardedObject: T;
  public
    class operator Implicit(GuardedObject: T): SmartGuard<T>;
    class operator Implicit(Guard: SmartGuard<T>): T;
  end;

implementation

{ TGuard }

constructor TGuard.Create(AObject: TObject);
begin
  FObject := AObject;
end;

destructor TGuard.Destroy;
begin
  FObject.Free;
  inherited;
end;

{ SmartGuard }

class operator SmartGuard<T>.Implicit(GuardedObject: T): SmartGuard<T>;
begin
  Result.FGuard := TGuard.Create(GuardedObject);
  Result.FGuardedObject := GuardedObject;
end;

class operator SmartGuard<T>.Implicit(Guard: SmartGuard<T>): T;
begin
  Result := Guard.FGuardedObject;
end;

end.

Sample code

Here is a simple (braindead?) program that uses them. It defines a simple but talkative class that shows when it is being destroyed, and which has a name. The demo shows how it can be used with the SmartGuard.

program SmartGuardDemo;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.SysUtils,
  Velthuis.SmartGuards in 'Velthuis.SmartGuards.pas';

type
  TTalker = class
  private
    FName: string;
  public
    constructor Create(const Name: string);
    destructor Destroy; override;
    procedure Talk;
  end;

{ TTalker }

constructor TTalker.Create(const Name: string);
begin
  FName := Name;
end;

destructor TTalker.Destroy;
begin
  Writeln(FName, ' is being destroyed');
  inherited;
end;

procedure TTalker.Talk;
begin
  Writeln(FName, ' is talking');
end;

procedure Test;
var
  A, B: TTalker;
  I: Integer;
begin
  Writeln('Creating object London...');
  A := SmartGuard<TTalker>(TTalker.Create('London'));
  Writeln('Creating object Paris...');
  B := SmartGuard<TTalker>(TTalker.Create('Paris'));
  A.Talk;
  B.Talk;
  Writeln('OK, so they talked');
  for I := 1 to 100 do
    Write(I:8);
  Writeln;
  Writeln;
  Writeln('Leaving the test');
end;

begin
  Test;
  Readln;
end.

Conclusion

I am sure that these smart pointers could do with some enhancements, and that there could be smart pointers for plain pointers/memory blocks too, or smart pointers that can be stored in lists, arrays, etc. and keep their guarded objects alive for longer than a function. This is just the base frame I have been thinking of.

As was said in the StackOverflow discussion, smart pointers are not a known Delphi idiom. I think that is a pity. But that would require a standard implementation (or a number of them) in the Delphi runtime library. Perhaps one day, we will see them there. Or they will be superseded by ARC. Who knows?

Rudy Velthuis