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 theGetRef
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 tonil
. 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
andRelease
take care of the reference counting:Retain
increments the reference count (if the object isn’tnil
). 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 tonil
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 andFRefCount
tonil
. - The
Retain
andRelease
operators now work on theFRefCount
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
andRelease
methods use theGetRefCountPtr
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 tonil
. 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”