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
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.
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
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;
Initializeoperator 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
Finalizeoperator is called in the
finallysection of the hidden
try..finallyblock, so this is where you call
EndUpdate. You have to check if the control is assigned first though, since it can be
nilif 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
Assignoperator 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
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
Assignoperators 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
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
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
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
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;
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
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…