There are those common try..finally
blocks that you write over and over again. For example entering and leaving a critical section to protect resources from multiple threads, or calling BeginUpdate
and EndUpdate
to efficiently bulk-update a FireMonkey control. These kinds of “restorable” operations can be automated with the new Custom Managed Records feature introduced in Delphi 10.4.

About Custom Managed Records
If you are not yet familiar with Custom Managed Records (called CMRs from here on out), then take a look at my previous post about using CMRs to wrap C(++) APIs.
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 key to automate “restorable” operations.
For more information about Custom Managed Records, take a look at the official documentation, my previous post or other resources on the internet.
The remainder of this post shows 3 ways you can use CMRs to automate restorable operations.
1. Automatic BeginUpdate and EndUpdate
Lets start with a simple example: All FireMonkey controls have BeginUpdate
and EndUpdate
methods that you should call to efficiently bulk-update a control. When methods like these come in pairs, you instinctively use a try..finally
block to ensure that EndUpdate
is always called:
ListView.BeginUpdate;
try
ListView.Items.Add.Text := 'Item 1';
ListView.Items.Add.Text := 'Item 2';
finally
ListView.EndUpdate;
end;
Whenever you see patterns like this, there is a potential for automating this with a CMR:
type
TAutoUpdate = record
private
FControl: TControl; // Reference
public
constructor Create(const AControl: TControl);
public
class operator Initialize(out ADest: TAutoUpdate);
class operator Finalize(var ADest: TAutoUpdate);
class operator Assign(var ADest: TAutoUpdate;
const [ref] ASrc: TAutoUpdate);
end;
constructor TAutoUpdate.Create(const AControl: TControl);
begin
Assert(Assigned(AControl));
FControl := AControl;
FControl.BeginUpdate;
end;
class operator TAutoUpdate.Initialize(out ADest: TAutoUpdate);
begin
ADest.FControl := nil;
end;
class operator TAutoUpdate.Finalize(var ADest: TAutoUpdate);
begin
if (ADest.FControl <> nil) then
ADest.FControl.EndUpdate;
end;
class operator TAutoUpdate.Assign(var ADest: TAutoUpdate;
const [ref] ASrc: TAutoUpdate);
begin
raise EInvalidOperation.Create(
'TAutoUpdate records cannot be copied')
end;
Some notes:
- The
Initialize
operator just clears the reference to the control. This is a very common pattern that you see a lot with CMRs. Note that you don’t have to add this operator, but we discuss below why you should. - You pass the control you want to auto-update to the constructor. The constructor immediately calls
BeginUpdate
on it. - The
Finalize
operator is called in thefinally
section of the hiddentry..finally
block, so this is where you callEndUpdate
. You have to check if the control is assigned first though, since it can benil
if the constructor hasn’t been called. - And finally, these kinds of CMRs should not be copyable (which is explained below). So we raise an exception in the
Assign
operator if you try to copy it anyway.
We can now rewrite the example snippet like this:
var AutoUpdate := TAutoUpdate.Create(ListView);
ListView.Items.Add.Text := 'Item 1';
ListView.Items.Add.Text := 'Item 2';
You just create the auto-update CMR, which will immediately call BeginUpdate
. Then when the CMR goes out of scope (at the end of the method), the EndUpdate
method will automatically be called because of the hidden try..finally
block and Finalize
operator.
This CMR saves 4 lines of code every time you use it, which I think is pretty great (unless you get paid by the line of course).
CMRs work great with inline variables, as in this example. When you declare the AutoUpdate
variable as a “normal” local variable, then the Initialize
operator will be called as soon as you enter the method. When using an inline variable instead, the Initialize
operator will be called at location where you declare the inline variable, which gives you more control.
These kind of CMRs are very common in C++ (although they are not called CMR in C++). In C++, constructors and destructors are called automatically when an object/struct is declared on the stack. And C++ has copy constructors as well, which are similar to
Assign
operators in Delphi.
Why the Initialize operator?
So what happens if you don’t write an Initialize
operator? In that case, the CMR will not be initialized at all (also not by the compiler). That means that the FControl
field in the sample CMR contains random data. This should’t be an issue if you always call the constructor, but what happens if you don’t?
begin
var AutoUpdate: TAutoUpdate;
end;
In this example, you just declare the CMR without constructing it. If there was no Initialize
operator, then its FControl
field would contain random data. Then, when the CMR goes out of scope, the Finalize
operator gets called, which calls the EndUpdate
method on an invalid object, most likely resulting in an access violation.
Why the Assign operator?
As I mentioned above, these kinds of CMRs should not be copyable. But why? Suppose you have the following code:
var AutoUpdate := TAutoUpdate.Create(ListView);
var AutoUpdateCopy := AutoUpdate;
ListView.Items.Add.Text := 'Item 1';
ListView.Items.Add.Text := 'Item 2';
This highlighted line in this example will call the Assign
operator if available, or otherwise perform a regular record copy. Now, when the copy goes out of scope, its Finalize
operator is called, which calls EndUpdate
. Then when the original goes out of scope, EndUpdate
is called again, resulting in an unbalanced BeginUpdate
/EndUpdate
pair.
Fortunately, there is no reason to copy these kinds of CMRs. To make sure you don’t copy the CMR by mistake, you should write an Assign
operator and handle that situation. In this example, I raise an exception if you attempt to make a copy. But you could also handle this situation in different ways. For example, you could set the FControl
field of target CMR to nil
instead, which would prevent a call to EndUpdate
.
2. Automatic Locking
A common design pattern in multi-threading is to enter a lock to protect a resource, then use that resource and finally release the lock again. Of course, you wrap this inside a try..finally
block to ensure that the lock is always released. Consider this simple locked list:
type
TLockedList<T> = class
private
FList: TList<T>;
FLock: TSynchroObject;
public
constructor Create;
destructor Destroy; override;
procedure Add(const AItem: T);
end;
constructor TLockedList<T>.Create;
begin
inherited;
FList := TList<T>.Create;
FLock := TCriticalSection.Create;
end;
destructor TLockedList<T>.Destroy;
begin
FLock.Free;
FList.Free;
inherited;
end;
procedure TLockedList<T>.Add(const AItem: T);
begin
FLock.Acquire;
try
FList.Add(AItem);
finally
FLock.Release;
end;
end;
The Add method could take advantage of a CMR:
procedure TLockedList<T>.Add(const AItem: T);
begin
var AutoLock := TAutoLock.Create(FLock);
FList.Add(AItem);
end;
And the CMR could look like this:
type
TAutoLock = record
private
FSyncObj: TSynchroObject; // Reference
public
constructor Create(const ASyncObj: TSynchroObject);
public
class operator Initialize(out ADest: TAutoLock);
class operator Finalize(var ADest: TAutoLock);
class operator Assign(var ADest: TAutoLock;
const [ref] ASrc: TAutoLock);
end;
constructor TAutoLock.Create(const ASyncObj: TSynchroObject);
begin
Assert(Assigned(ASyncObj));
FSyncObj := ASyncObj;
FSyncObj.Acquire;
end;
class operator TAutoLock.Initialize(out ADest: TAutoLock);
begin
ADest.FSyncObj := nil;
end;
class operator TAutoLock.Finalize(var ADest: TAutoLock);
begin
if (ADest.FSyncObj <> nil) then
ADest.FSyncObj.Release;
end;
class operator TAutoLock.Assign(var ADest: TAutoLock;
const [ref] ASrc: TAutoLock);
begin
raise EInvalidOperation.Create(
'TAutoLock records cannot be copied');
end;
This CMR is very similar to the TAutoUpdate
CMR presented earlier, so I won’t go into the details here. Note that you can use it with any type of synchronization object (critical section, mutex, etc…).
This CMR is so common in C++, that it is part of the standard library under the name
std::lock_guard
.
3. Automatic Object Destruction
So what about the obvious use case: automatic object destruction? You can certainly use a similar CMR for this:
type
TAutoFree = record
private
FInstance: TObject; // Reference
public
constructor Create(const AInstance: TObject);
class operator Initialize(out ADest: TAutoFree);
class operator Finalize(var ADest: TAutoFree);
class operator Assign(var ADest: TAutoFree;
const [ref] ASrc: TAutoFree);
end;
constructor TAutoFree.Create(const AInstance: TObject);
begin
Assert(Assigned(AInstance));
FInstance := AInstance;
end;
class operator TAutoFree.Initialize(out ADest: TAutoFree);
begin
ADest.FInstance := nil;
end;
class operator TAutoFree.Finalize(var ADest: TAutoFree);
begin
ADest.FInstance.Free;
end;
class operator TAutoFree.Assign(var ADest: TAutoFree;
const [ref] ASrc: TAutoFree);
begin
raise EInvalidOperation.Create(
'TAutoFree records cannot be copied')
end;
This is similar to an
std::unique_ptr
in C++
You can use it like this:
begin
var Writer := TStreamWriter.Create('foo.txt');
var AutoFree := TAutoFree.Create(Writer);
Writer.WriteLine('Some Text');
OpenFile('foo.txt');
end;
This example has a problem though: the TStreamWriter
object will automatically be destroyed when the AutoFree
CMR goes out of scope. This is at the end of the method, after the OpenFile
call. This means that OpenFile
call may fail to open the file because the stream writer still has an exclusive lock on it. With the help of inline variables though, we can scope the AutoFree
CMR and ensure that the stream writer is destroyed before the file is opened:
begin
var Writer := TStreamWriter.Create('foo.txt');
begin
var AutoFree := TAutoFree.Create(Writer);
Writer.WriteLine('Some Text');
end;
OpenFile('foo.txt');
end;
Now, the AutoFree
CMR goes out of scope before OpenFile
is called. Note that you can also put the stream writer construction inside the inner scope.
While this CMR can be useful, it cannot be used to automatically manage the lifetime of an object in situations where the object is shared. For those situations, you would traditionally use object interfaces to take advantage of automatic reference counting.
But with CMRs, there is an alternative now: a CMR can be used to add automatic reference counting to regular objects.
Which would be similar to an
std::shared_ptr
in C++
And that is were we enter the subject of “smart pointers”, which is outside the scope of this article, but could be come the subject of a future article…
The TAutoFree functionality was implemented in the mORMot framework. It uses interface reference counting for this.
LikeLike
This type of “restorable action” can also be accomplished by hacking the Disposable pattern, in .NET world.
LikeLike
A CMR is somewhat similar to the Disposable pattern, but completely automatic. It is more like a C++ struct though.
LikeLike