Foundation · Patterns · Tips & Tricks · Uncategorized

Custom Managed Records for Smart Pointers

In this 3rd part in (what turns out to be) a series about Custom Managed Records, we take a look at how they can be used to create smart pointers.

About Smart Pointers

But first we have to get some terminology straight. Smart Pointers is an umbrella term behind different technologies for making it easier to manage the lifetime of an object. In my previous article, I showed a simple TAutoFree CMR (Custom Managed Record) that can be used to automatically free an object when the CMR goes out of scope. This is somewhat similar to the std::unique_ptr type in C++.

This isn’t a very “smart” CMR though: you cannot have multiple TAutoFree records referring to the same object, since each of those would destroy the object when it goes out of scope, leading to double destruction. We tried to prevent this by not allowing a TAutoFree CMR to be copied.

In this post we focus on another type of smart pointer: one that can be copied (or shared) and uses automatic reference counting (ARC) to keep track of the number of smart pointers that is referring to an object. When the reference count reaches 0, there are no more references to the object so it can be destroyed. This is more like the std::shared_ptr type in C++.

This type of smart pointer can be used to automate object lifetime management. Before Delphi 10.4 the mobile compilers used ARC to manage the lifetime of all objects. Although I personally liked this feature, it introduced the problem of not being compatible with memory management models on desktop platforms. As a result, you could never take advantage of this feature for developing cross-platform code. Now that ARC for objects no longer exists, you can use smart pointers to regain some of its advantages. But this time, you can use it selectively and only in places where it makes life easier and less prone to memory leaks.

In this post, I will show you 4 possible ways to create a smart pointer: one using object interfaces and 3 using CMRs. The code in this article can be found in our JustAddCode repository on GitHub. But first, lets recap what a Custom Managed Record is.

Custom Managed Record Recap

Custom Managed Records were added to the language in Delphi 10.4. In the first part of this series, I explained this new feature and how it can be used to wrap 3rd party C(++) APIs. In the second part, I showed how you can use it to automate “restorable operations”, such as automatically entering and leaving a lock. You don’t need to read these two parts to understand this post. So I took the liberty of copying the introduction to CMRs from the second part. Feel free to skip this if you are already familiar with CMRs.

As a short recap, consider the following CMR:

type
  TFoo = record
  public
    class operator Initialize(out ADest: TFoo);
    class operator Finalize(var ADest: TFoo);
    class operator Assign(var ADest: TFoo;
      const [ref] ASrc: TFoo);
  end;

Then Delphi will automatically call the Initialize operator when you declare such a record, and Finalize when the record goes out of scope. The Assign operator is called to copy one record to another one. You don’t have to use all 3 operators, but you need at least one to create a CMR.

Now, when you write the following code:

begin
  var Foo1: TFoo;
  var Foo2 := Foo1;
end;

Then Delphi will convert this to the following code behind the scenes (note that this code does not compile though):

begin
  var Foo1, Foo2: TFoo;
  TFoo.Initialize(Foo1);
  try
    TFoo.Initialize(Foo2);
    try
      TFoo.Assign(Foo2, Foo1);
    finally
      TFoo.Finalize(Foo2);
    end;
  finally
    TFoo.Finalize(Foo1);
  end;
end;

Delphi will automatically insert try..finally statements to make sure that the Finalize operator is always called. This is the essential feature to make smart pointers work.

1. Using Object Interfaces

Since interfaces already provide automatic reference counting, they are an obvious candidate for smart pointers. A simple implementation may look like this:

type
  ISmartPtr<T: class> = interface
    function GetRef: T;

    property Ref: T read GetRef;
  end;

type
  TSmartPtr<T: class> = class(TInterfacedObject, ISmartPtr<T>)
  private
    FRef: T;
  protected
    { ISmartPtr<T> }
    function GetRef: T;
  public
    constructor Create(const ARef: T);
    destructor Destroy; override;
  end;

{ TSmartPtr<T> }

constructor TSmartPtr<T>.Create(const ARef: T);
begin
  inherited Create;
  FRef := ARef;
end;

destructor TSmartPtr<T>.Destroy;
begin
  FRef.Free;
  inherited;
end;

function TSmartPtr<T>.GetRef: T;
begin
  Result := FRef;
end;

We use a generic interface here to add type safety and avoid typecasting the referenced object. The type parameter T has a class constraint so you can only use the smart pointer with objects. The Ref property just returns the referenced object (of type T). The smart pointer becomes owner of the object and frees it in the destructor. It is safe to copy the smart pointer since reference counting will ensure that the object isn’t destroyed until the last smart pointer referring to it goes out of scope.

The following example shows how this smart pointer can be used to wrap a TStringList:

var List1: ISmartPtr<TStringList> :=
  TSmartPtr<TStringList>.Create(TStringList.Create);

{ The smart pointer has a reference count of 1.
  Add some strings }
List1.Ref.Add('Foo');
List1.Ref.Add('Bar');

begin
  { Copy the smart pointer.
    It will have a reference count of 2 now. }
  var List2 := List1;

  { Check contents of List2 }
  Assert(List2.Ref[0] = 'Foo');
  Assert(List2.Ref[1] = 'Bar');

  { List2 will go out of scope here, so only List1
    will keep a reference to the TStringList.
    The reference count will be reduced to 1. }
end;

{ Check contents of List1 again }
Assert(List1.Ref.Count = 2);

{ Now List1 will go out of scope, reducing the
  reference count to 0 and destroying the TStringList. }

Important to note here is that you cannot use type inference to create an instance of the smart pointer. That is, this code is wrong:

var List1 := TSmartPtr<TStringList>.Create(TStringList.Create);

This will make List1 of type TSmartPtr<> instead of ISmartPtr<>, resulting in two memory leaks (for the TSmartPtr<> object itself and for the TStringList). You have to explicitly specify the type:

var List1: ISmartPtr<TStringList> :=
  TSmartPtr<TStringList>.Create(TStringList.Create);

This is one of the reasons why interfaces aren’t an ideal solution for smart pointers: it is relatively easy to unintentionally create a memory leak. Other reasons why interfaces aren’t the best tool for this job include:

  • It requires a separate object allocation just to keep track of another object. This increases memory usage and the potential for memory fragmentation.
  • Every access to the Ref property goes through the GetRef method. Because every method in an interface is virtual, this means an extra level of indirection. This extra lookup trashes the cache, and together with the overhead of a method call, this can have a negative impact on performance.

Object interfaces are a great way to separate specification from implementation, and do provide the benefit of automatic lifetime management. As such, you should use them for many everyday programming problems.

However, there are many classes in the RTL or third party libraries that don’t use interfaces and cannot take advantage of ARC. So you need some kind of smart pointer if you want to automate lifetime management. I don’t think that wrapping them inside an interface as seen above is a very good solution though. Custom Managed Records provide a better alternative…

2. Using a CMR with a Base Class

So how can a Custom Managed Record help here? We need a way to reference count objects. Your initial instinct may be to add both the object and a reference count to a CMR:

type
  TSmartPtr<T: class> = record
  private
    FRef: T;
    FRefCount: Integer;
  public
    constructor Create(const ARef: T);
    class operator Initialize(out ADest: TSmartPtr<T>);
    class operator Finalize(var ADest: TSmartPtr<T>);
    class operator Assign(var ADest: TSmartPtr<T>;
      const [ref] ASrc: TSmartPtr<T>);

    property Ref: T read FRef;
  end;

Then, in the constructor and the Assign and Finalize operators, you would manipulate the reference count and free the object if it reaches 0. However, this will not work if you assign one record to another. In that case, both copies will end up with their own reference counts. And the reference count of one record may reach 0 (and destroy the referenced object) while the other record still has a reference to it.

So the reference count needs to be part of the object. One way to do this is to create a base class that contains just a reference count so it can be used with a smart pointer:

type
  TRefCountable = class abstract
  protected
    FRefCount: Integer;
  end;

The FRefCount field should probably have a [volatile] attribute, but that is outside the scope of this article.

Then we can add a generic type constraint to the smart pointer so it can only be used with subclasses of TRefCountable:

type
  TSmartPtr<T: TRefCountable> = record
  private
    FRef: T;
    procedure Retain; inline;
    procedure Release; inline;
  public
    constructor Create(const ARef: T);
    class operator Initialize(out ADest: TSmartPtr<T>);
    class operator Finalize(var ADest: TSmartPtr<T>);
    class operator Assign(var ADest: TSmartPtr<T>;
      const [ref] ASrc: TSmartPtr<T>);

    property Ref: T read FRef;
  end;

And the implementation can look like this:

constructor TSmartPtr<T>.Create(const ARef: T);
begin
  Assert((ARef = nil) or (ARef.FRefCount = 0));
  FRef := ARef;
  Retain;
end;

class operator TSmartPtr<T>.Initialize(out ADest: TSmartPtr<T>);
begin
  ADest.FRef := nil;
end;

class operator TSmartPtr<T>.Finalize(var ADest: TSmartPtr<T>);
begin
  ADest.Release;
end;

procedure TSmartPtr<T>.Retain;
begin
  if (FRef <> nil) then
    AtomicIncrement(FRef.FRefCount);
end;

procedure TSmartPtr<T>.Release;
begin
  if (FRef <> nil) then
  begin
    if (AtomicDecrement(FRef.FRefCount) = 0) then
      FRef.Free;

    FRef := nil;
  end;
end;

class operator TSmartPtr<T>.Assign(var ADest: TSmartPtr<T>;
  const [ref] ASrc: TSmartPtr<T>);
begin
  if (ADest.FRef <> ASrc.FRef) then
  begin
    ADest.Release;
    ADest.FRef := ASrc.FRef;
    ADest.Retain;
  end;
end;

This is how it works:

  • The Initialize operator just sets the reference to nil. This is a very common pattern for CMRs.
  • The constructor sets the reference to the object (derived from TRefCountable) and retains it (more about that below). We perform a sanity check to make sure that the same object cannot passed to the constructor of multiple smart pointers (that is, its reference count should be 0).
  • The Finalize operator just releases the reference, which may result in freeing the object.
  • The helper methods Retain and Release take care of the reference counting:
    • Retain increments the reference count (if the object isn’t nil). It does this in an atomic way so that smart pointers can be shared across multiple threads.
    • Likewise, Release decrements the reference count. When it reaches 0, it will destroy the referenced object. In any case, it sets the reference to nil since it shouldn’t be used anymore.
  • The Assign operator is the most complicated one (relatively speaking). First it checks if the two smart pointers already reference the same object. If so, there is nothing to be done. If not, it first releases the target smart pointer since it is going to be assigned a new one. This may result in the destruction of the object if no other smart pointers have a reference to it. Then, it copies the reference and retains it. This is a common pattern, that is also used by the RTL when assigning object interfaces.

This smart pointer can be used as follows:

type
  TFoo = class(TRefCountable)
  private
    FIntVal: Integer;
    FStrVal: String;
  public
    property IntVal: Integer read FIntVal write FIntVal;
    property StrVal: String read FStrVal write FStrVal;
  end;

procedure TestSmartPointer;
begin
  var Foo1 := TSmartPtr<TFoo>.Create(TFoo.Create);
  { The smart pointer has a reference count of 1. }

  { Set some properties }
  Foo1.Ref.IntVal := 42;
  Foo1.Ref.StrVal := 'Foo';

  begin
    { Copy the smart pointer.
      It has a reference count of 2 now. }
    var Foo2 := Foo1;

    { Check properties }
    Assert(Foo2.Ref.IntVal = 42);
    Assert(Foo2.Ref.StrVal = 'Foo');

    { Foo2 will go out of scope here, so only Foo1
      will keep a reference to the TFoo object.
      The reference count will be reduced to 1. }
  end;

  { Check properties again }
  Assert(Foo1.Ref.IntVal = 42);

  { Now Foo1 will go out of scope, reducing the
    reference count to 0 and destroying the TFoo object. }
end;

You can use type inference now, as in:

var Foo1 := TSmartPtr<TFoo>.Create(TFoo.Create);

You could not do this when using object interfaces for smart pointers.

Of course, the big disadvantage of this method is that it can only be used with classes derived from TRefCountable. You cannot use this smart pointer with a TStringList or any other RTL class.

3. Using a CMR with a Shared Reference Count

One way to overcome this issue is to dynamically allocate the reference count so it can be shared with copies of the smart pointer:

type
  TSmartPtr<T: class> = record
  private
    FRef: T;
    FRefCount: PInteger;
    procedure Retain; inline;
    procedure Release; inline;
  public
    constructor Create(const ARef: T);
    class operator Initialize(out ADest: TSmartPtr<T>);
    class operator Finalize(var ADest: TSmartPtr<T>);
    class operator Assign(var ADest: TSmartPtr<T>;
      const [ref] ASrc: TSmartPtr<T>);

    property Ref: T read FRef;
  end;

The FRefCount field is not an Integer now, but a pointer to an integer. The implementation is slightly more complicated since we need to allocate and free this field when appropriate:

constructor TSmartPtr<T>.Create(const ARef: T);
begin
  FRef := ARef;
  if (ARef <> nil) then
  begin
    GetMem(FRefCount, SizeOf(Integer));
    FRefCount^ := 0;
  end;
  Retain;
end;

class operator TSmartPtr<T>.Initialize(out ADest: TSmartPtr<T>);
begin
  ADest.FRef := nil;
  ADest.FRefCount := nil;
end;

class operator TSmartPtr<T>.Finalize(var ADest: TSmartPtr<T>);
begin
  ADest.Release;
end;

procedure TSmartPtr<T>.Retain;
begin
  if (FRefCount <> nil) then
    AtomicIncrement(FRefCount^);
end;

procedure TSmartPtr<T>.Release;
begin
  if (FRefCount <> nil) then
  begin
    if (AtomicDecrement(FRefCount^) = 0) then
    begin
      FRef.Free;
      FreeMem(FRefCount);
    end;

    FRef := nil;
    FRefCount := nil;
  end;
end;

class operator TSmartPtr<T>.Assign(var ADest: TSmartPtr<T>;
  const [ref] ASrc: TSmartPtr<T>);
begin
  if (ADest.FRef <> ASrc.FRef) then
  begin
    ADest.Release;
    ADest.FRef := ASrc.FRef;
    ADest.FRefCount := ASrc.FRefCount;
    ADest.Retain;
  end;
end;

The differences compared to version 2 are:

  • The constructor allocates memory for the FRefCount field. Since dynamically allocated memory contains random data, it must set this field to 0.
  • The Initialize operator sets both the reference and FRefCount to nil.
  • The Retain and Release operators now work on the FRefCount field (if assigned). When the reference count reaches 0, it not only frees the object, but also the memory for the reference count itself.
  • Finally, the Assign operator stays mostly the same. You only need to make sure you copy both the reference and the reference count pointer.

You would use this smart pointer in exactly the same way as in version 2. The advantage now is that you can use this smart pointer with any class, and not just with classes derived from TRefCountable.

But there is the disadvantage that you need to dynamically allocate a very small (4 bytes) piece of memory just to store the reference count. This not only results in increased memory usage (since the memory manager allocates more than just 4 bytes), but also increases memory fragmentation.

The last version tries to avoid this.

4. Using a CMR with a Monitor Hack

So the main problem is that we need to associate a reference count with an object. Version 2 did this by defining a base class with a reference count, and version 3 used a dynamically allocated reference count. So is there a way to create a smart pointer that doesn’t require a base class or dynamically allocated field? That is, is there another way to associate a reference count with an object?

In fact there is, but it is kind of a hack, so you may be a bit uncomfortable with it. You may or may not know that every object has a hidden pointer-sized field that is used when the object is used as a monitor. This is the case when you pass the object to TMonitor.Enter or TMonitor.Leave to use the object as a lock in multi-threaded scenarios. I personally always considered this hidden field a waste of memory, since only a very small fraction of objects are used as monitors. Furthermore, even though any object can be used as a monitor, it is recommended to create separate objects just for this purpose (which is another reason why adding this functionality at the TObject level is a waste IMHO).

But we can take advantage of this hidden field and use it to store the reference count instead. As long as you don’t use the object as a monitor, that is totally fine.

So how can we get to this hidden field? Delphi adds this hidden field as the very last field of any class. Even when you create a subclass and add new fields to it, Delphi will make sure that the monitor field is added after all the custom fields you added. So to get to the field, you have to find the memory location of the end of the object, and subtract a pointer-sized value. The System unit defines the constants hfFieldSize and hfMonitorOffset to help with this. So for any object, you can find a pointer to the monitor as follows:

function GetMonitorPtr(const AObj: TObject): Pointer;
begin
  Result := Pointer(IntPtr(AObj) + AObj.InstanceSize 
    - hfFieldSize + hfMonitorOffset);
end;

The IntPtr(AObj) + AObj.InstanceSize part calculates the address just passed the end of the object. Then, it subtracts the size of a pointer (hfFieldSize) and adds the offset to the monitor field (hfMonitorOffset, which is 0 in the current Delphi version).

This trick can the be used to create the following smart pointer:

type
  TSmartPtr<T: class> = record
  private
    FRef: T;
    function GetRefCountPtr: PInteger; inline;
    procedure Retain; inline;
    procedure Release; inline;
  public
    constructor Create(const ARef: T);
    class operator Initialize(out ADest: TSmartPtr<T>);
    class operator Finalize(var ADest: TSmartPtr<T>);
    class operator Assign(var ADest: TSmartPtr<T>;
      const [ref] ASrc: TSmartPtr<T>);

    property Ref: T read FRef;
  end;

This is very similar to versions 2 and 3, but it adds the method GetRefCountPtr that returns the address of the reference count (monitor) field:

constructor TSmartPtr<T>.Create(const ARef: T);
begin
  FRef := ARef;
  Assert((FRef = nil) or (GetRefCountPtr^ = 0));
  Retain;
end;

class operator TSmartPtr<T>.Initialize(out ADest: TSmartPtr<T>);
begin
  ADest.FRef := nil;
end;

class operator TSmartPtr<T>.Finalize(var ADest: TSmartPtr<T>);
begin
  ADest.Release;
end;

function TSmartPtr<T>.GetRefCountPtr: PInteger;
begin
  if (FRef = nil) then
    Result := nil
  else
    Result := PInteger(IntPtr(FRef) + FRef.InstanceSize
      - hfFieldSize + hfMonitorOffset);
end;

procedure TSmartPtr<T>.Retain;
begin
  var RefCountPtr := GetRefCountPtr;
  if (RefCountPtr <> nil) then
    AtomicIncrement(RefCountPtr^);
end;

procedure TSmartPtr<T>.Release;
begin
  var RefCountPtr := GetRefCountPtr;
  if (RefCountPtr <> nil) then
  begin
    if (AtomicDecrement(RefCountPtr^) = 0) then
      FRef.Free;

    FRef := nil;
  end;
end;

class operator TSmartPtr<T>.Assign(var ADest: TSmartPtr<T>;
  const [ref] ASrc: TSmartPtr<T>);
begin
  if (ADest.FRef <> ASrc.FRef) then
  begin
    ADest.Release;
    ADest.FRef := ASrc.FRef;
    ADest.Retain;
  end;
end;

The implementation only differs slightly from versions 2 and 3:

  • The Retain and Release methods use the GetRefCountPtr helper to retrieve a pointer to the hidden monitor field. It used this address to store the reference count instead.
  • Before we destroy the object, we need to make sure that the hidden monitor field is set to nil. Otherwise, the destructor of the object will try to free the monitor, which isn’t actually a monitor, but a reference count. That would most likely result in an access violation. Fortunately, the object is only freed when the reference count has reached 0, which has the same effect as setting the monitor field to nil. So we don’t need to do anything extra.

Again, you can use this smart pointer in the exact same way as in versions 2 and 3. It has the advantage that you can use it with any object, and it doesn’t require any additional dynamically allocated memory.

Even though this is a bit of a hack, it works very well. The only thing you need to keep in mind is that you can’t use the object as a monitor (which you probably aren’t doing anyway).

Disadvantages

As with everything, smart pointers have some disadvantages as well:

1. First, smart pointers incur a slight performance penalty due to the reference counting and the hidden calls to the Initialize, Finalize and Assign operators.

2. Also, as with object interfaces, you can create a reference cycle:

type
  TBar = class;

  TFoo = class
  private
    FBar: TSmartPtr<TBar>;
  public
    property Bar: TSmartPtr<TBar> read FBar write FBar;
  end;

  TBar = class
  private
    FFoo: TSmartPtr<TFoo>;
  public
    property Foo: TSmartPtr<TFoo> read FFoo write FFoo;
  end;

procedure MakeReferenceCycle;
begin
  { Create smart pointers for TFoo and TBar }
  var Foo := TSmartPtr<TFoo>.Create(TFoo.Create);
  var Bar := TSmartPtr<TBar>.Create(TBar.Create);

  { Make a reference cycle }
  Foo.Ref.Bar := Bar;
  Bar.Ref.Foo := Foo;
end;

This results in a memory leak because both the Foo and Bar smart pointers have a strong reference to each other, and they will never be destroyed unless the cycle is broken. You cannot use [weak] or [unsafe] attributes with smart pointers. A solution may be to manually break the cycle at some point, or to not use a smart pointer at one of the two sides. Or you can create something like the std::weak_ptr type in C++.

3. Another disadvantage is that you have to type .Ref any time you want to access the referenced object:

var List := TSmartPtr<TStringList>.Create(TStringList.Create);
List.Ref.Add('Foo');
List.Ref.Add('Bar');

It would be nice if Delphi provided operator overloading for the “^” (or even “.“) operator, so this code can be rewritten as:

var List := TSmartPtr<TStringList>.Create(TStringList.Create);
List^.Add('Foo');
List^.Add('Bar');

Maybe in a future Delphi version…

4. Likewise, writing TSmartPtr<TStringList> every time may become a bit cumbersome. Although you can mitigate this by defining helper types for regularly used smart pointers:

type
  TSPStringList = TSmartPtr<TStringList>;

begin
  var List := TSPStringList.Create(TStringList.Create);
end;

You can use some sort of naming convention (like the TSP* prefix) to make it clear that the type is a smart pointer.

5. You may still find it strange that you have to create a TStringList first and then pass that new object to the constructor of the smart pointer. You can have the smart pointer create the object for you if you add a constructor constraint to the generic type parameter:

type
  TSmartPtr<T: class, constructor> = record
  private
    FRef: T;
  public
    class function Create: TSmartPtr<T>; static;
    ...
  end;

{ TSmartPtr<T> }

class function TSmartPtr<T>.Create: TSmartPtr<T>;
begin
  Result.FRef := T.Create;
  ...
end;

Then you can use it like this instead:

type
  TSPStringList = TSmartPtr<TStringList>;

begin
  var List := TSPStringList.Create;
end;

Of course, this only works with classes that have a parameterless constructor.

You may also prefer not to use a name like “Create” to create a smart pointer, since many people associate this with object construction. You could use a name like “Make” or “New” instead.

6. And finally, you may be old school and just prefer to always manually keep track of the objects you create. This way, you are in total control of when an object gets destroyed. This is totally fine of course: you should use whatever you are comfortable with.

I certainly don’t want to bring up the manual-vs-automatic memory management discussion again. Both approaches have their pros and cons.

But sometimes, there are complicated relationships between objects and it can become hard to track of who owns an object and is responsible for freeing it. In those cases, smart pointers (or object interfaces) can help.

Conclusion

That is not a small list of disadvantages. But fortunately, most of them are minor and will have little impact or can easily be worked around.

Also, these are not the only ways you can implement smart pointers. There have been numerous more or less successful attempts at creating smart pointers in the past using Delphi. For example, Rudy Velthuis showed an approach that uses a record with an interface that guards the object. Barry Kelly shows a quite ingenious approach that uses anonymous methods, and Jarrod Hollingworth expands on that with his own smart pointer implementation.

Now, you can add Custom Managed Records to your toolbox to create smart pointers. I think these work pretty well. Especially version 4 above, which is quite efficient, doesn’t use additional memory and works with any kind of object.

If you want to give it a try, then head over to our JustAddCode repository for the sample implementations. You can modify them to your own liking.

One thought on “Custom Managed Records for Smart Pointers

Leave a comment