Custom Managed Records Coming in Delphi 10.3

by Nov 8, 2018

Beside inline variable declarations, Delphi 10.3 is going to offer another new Object Pascal language extension, custom managed records 

Beside inline variable declaration, as explained in my recent blog post, the coming 10.3 version of Delphi will have another relevant addition to the Delphi language, namely the ability to define code to be executed when records are first initialized, disposed from memory and copied. Before we get to that, though, let me recap a few key elements of records in today’s Delphi language.

Records in Recent Versions

In Modern Object Pascal (from Delphi 2009, if I remember) the record type constructor has gained many new features, from the ability to define methods to operators overloading. The main differentiator from class type remains the way memory is allocated. While a class based variable is just a reference to dynamically allocated memory, a record based variable has a copy of the actual data.

The core difference in memory layout is also related with the fact records lack virtual methods and inheritance, a key tenets of object-oriented programming. On the plus side, the way records are allocated in memory can help with memory management and make it faster – as the memory is directly available skipping an extra allocation and deallocation operation.

The other large difference we don’t have time to explore in full is what happens when you pass a record as parameter to a function. By default, the compiler makes a copy of the entire record data. You can avoid the copy by passing the record by reference (var). These has deeper implications than it might seem.

Standard and Managed Records

Records can of course have fields of any type. When a record has plain (non-managed) fields, like numeric or other enumerated values there isn’t much to do for the compiler and disposing the record consist of getting rid of the memory location – and likely making it available to be reused later. But if a record has a field of a type managed (in terms of memory) by the compiler, things get slightly more complex. An example would be a record with a string field. The string itself is reference counted, so when the record goes out of scope the string inside the record needs have its reference count decreased, which might lead to de-allocating the memory for the string. Therefore, when you are using such a managed record in a section of the code, the compiler automatically adds a try-finally block around that code, and makes sure the data is cleared even in case of an exception.

Custom Managed Records

So if managed records have existing for a long time, what does it mean that Delphi 10.3 will add support for them? What the language will provide is “custom” managed record. In other words, you will be able to declare a record with custom initialization and finalization code regardless of the data type of its fields, and you’ll be able to write such custom initialization and finalization code. You’d do so by adding a parameter-less constructor to the record type and a destructor (you can have one without the other, if you want). Here is a simple code snippet:

  TMyRecord = record
    Value: Integer;
    constructor Create;
    destructor Destroy;

You’ll have of course to write the code of these two methods. The huge difference between this new constructor and what was previously available for records is the automatic invocation. In fact if you write something like:

procedure TForm5.btnMyRecordClick(Sender: TObject);
  my1: TMyRecord;
  Log (my1.Value.ToString);

you’ll end up invoking both the default constructor and the destructor, and you’ll end up with a try-finally block generated by the compiler for your managed record instance.

You can also explicitly invoke the record default constructor, in code like:

myrec := TMyRecord.Create;

There is of course the risk, depending on how you write the code, that the default constructor gets invoked twice, implicitly by the compiler and in your code. In case of inline variable declarations, this won’t happen, but in other cases it might.

The Assign Operator

Another new feature brought along with custom managed records is the ability to execute custom code to assign a record to another. Rather than copying the entire data, field by field, you might want to perform different tasks, keep a single copy of some of the fields, duplicate an object a record refers to, or any other custom operation. The new operator is invoked with the := syntax, but defined as “Assign”:

  TMyRecord = record
    Value: Integer;
    class operator Assign (var Dest: TMyRecord; const [ref] Src: TMyRecord);

The operator definition must follow very precise rules, including having the first parameter as a reference parameter, and the second as var or const passed by reference. If you fail to do so, the compiler issues  error messages like:

[dcc32 Error] E2617 First parameter of Assign operator must be a var parameter of the container type
[dcc32 Hint] H2618 Second parameter of Assign operator must be a const[Ref] or var parameter of the container type

The Assign operator is invoked if you write:

  my1, my2: TMyRecord;
  my1.Value := 22;
  my2 := my1;

The assign operator is used in conjunction with assignment operations like the one above, and also if you use an assignment to initialize a inline variable (in which case the default constructor is not called):

 var my4 := my1;

Copy Constructor

There is however another interesting scenario, which is the case you are passing a record parameter or want to create a new record based on an existing one. To handle similar scenarios you can define a Copy constructor, that is a constructor that takes a record of the same type as parameter. In most cases, you’d also want a default constructor, so you need to mark them with the overload directive:

  TMyRecord = record
    constructor Create; overload;
    constructor Create (const mr: TMyRecord); overload;

If you now define a method or procedure with a regular value parameter (not passed by const or var), when you invoke it, it will use the Copy constructor, not an assignment call:

procedure ProcessRec (mr: TMyRecord);
  Log (mr.Value.ToString);

Notice that you also get the Destroy destructor called at the end of the procedure. Another way to invoke the Copy Constructor is to use the following inline variable declaration:

 var my3 := TMyRecord.Create (my1);

Conclusion and Disclaimer

A very interesting use case for managed records is the implementation of smart pointers. I’ve written some code I’ll share in a future blog post. What I wanted to do today was only to share the overall concept.

This is another significant changes for the language, and it opens up even further use of record types. Notice, however, that some of the details of managed records are still subject to change until GA of Delphi 10.3.