Difference between revisions of "management operators"

From Free Pascal wiki
Jump to navigationJump to search
m (→‎A Simple example of use: {$H+} is superfluous in {$mode Delphi})
(→‎Copy: example)
 
(75 intermediate revisions by 4 users not shown)
Line 1: Line 1:
== Management Operators ==
+
== Management operators feature ==
From Free Pascal version 3.1.1 there is a new language feature called management operators for advanced records.
+
From Free Pascal version 3.1.1 onwards, there is a new language feature called management operators for extended or advanced records.
The new operators are: Initialize, Finalize, AddRef and Copy.<br>
+
 
The new operators are a unique feature and are called "management operators". That is because:<br>
+
The new operators are: Initialize, Finalize, AddRef and Copy.
* Each record,even non-managed or empty, with management operator becomes a managed type.<br>
+
 
* It is now possible to implement new custom types with their own memory management, e.g: new string types, fast TValue implementations without hacks on RTL etc.<br>
+
These are a fairly unique feature, and are called "management operators" because:
* Management operators have no result type as opposed to normal operators.<br>
+
 
* A simple virtual method table is generated for the management oprators. Thanks to this is possible to work with management operators together with all RTL functions like InitializeArray and FinalizeArray.
+
* Each record, even non-managed or empty, that implements any management operators becomes a managed type.
<br>
+
* They make it possible to implement new custom types with their own memory management, e.g: new string types, fast TValue implementations without hacks on the RTL, etc.
Management Operators features can be used for many things:
+
 
* Support for value types.
+
Management operators have no result type as opposed to normal operators, and work via a very simple VMT.
* Nullable types.
+
 
* Some custom ARC variations.
+
Thanks to this, it is possible to combine management operators with all low-level RTL functions, such as InitializeArray / FinalizeArray / etc.
* Speed up existing types.
+
 
* Very fast RTTI.TValue implementation.
+
Management operators can be used for many things:
* As replacement for manually called Init/Done record methods like in mORMot for many types (for example SynCommons.TSynLocker).
+
* More granularly controlling the lifetimes of simple value types / primitives
* Auto init/finit for pointers/classes/simple types or whatever we have in Pascal.
+
* Implementing "nullable" value types
<br>
+
* Custom ARC implementations
It works correctly in all possible ways with RTL:  
+
* A very fast RTTI.TValue implementation
 +
* As a replacement for manually-called Init/Done record methods like the popular "mORMot" library uses for many types (for example in SynCommons.TSynLocker).
 +
* Auto init/finit for pointers/classes/simple types or anything else we have in Pascal.
 +
* Much more
 +
 
 +
They work correctly in all possible ways with the RTL:  
 
* New (Initialize).
 
* New (Initialize).
 
* Dispose (Finalize).
 
* Dispose (Finalize).
Line 27: Line 32:
 
* Copy (AddRef).
 
* Copy (AddRef).
 
* RTTI.IsManaged.
 
* RTTI.IsManaged.
<br>
+
 
Managements operators are often called implicitly, for example:
+
Management operators, when implemented, are called implicitly at various times. For example:
 
* Global variables (Initialize/Finalize).
 
* Global variables (Initialize/Finalize).
 
* Local variables (Initialize/Finalize).
 
* Local variables (Initialize/Finalize).
 
* For fields inside records, objects or classes (Initialize/Finalize).
 
* For fields inside records, objects or classes (Initialize/Finalize).
 
* Variable assignment (Copy).
 
* Variable assignment (Copy).
* For parameters for routines - AddRef/Finalize/none - this depends on modifiers like var/constref/const.
+
* For parameters for routines - AddRef/Finalize/none - this depends on modifiers like var / constref / const.
  
<br>
+
== Initialize ==
=== Initialize ===
+
The initialize operator is called directly after stack (or heap) memory allocation for a record happens.
The initialize operator is called after memory allocation for a record and called after the internal compiler call to recordrtti(data,typeinfo,@int_initialize);
+
 
It allows automatic initialization for a record.
+
It allows for custom automatic initialization code.
A simple example is:
+
<syntaxhighlight lang="pascal">
<syntaxhighlight>
 
{$if FPC_FULLVERSION < 30101}{$ERROR this demo needs version 3.1.1}{$endif}
 
{$mode delphi}{$macro on}
 
 
program TestInitialize;
 
program TestInitialize;
 +
 +
{$if FPC_FULLVERSION < 30101}
 +
{$ERROR this demo needs version 3.1.1}
 +
{$endif}
 +
{$mode delphi}
 +
 +
type
 +
  PRec = ^TRec;
 +
 +
  TRec = record
 +
    I: Integer;
 +
    class operator Initialize(var aRec: TRec);
 +
  end;
 +
 +
  class operator TRec.Initialize(var aRec: TRec);
 +
  begin
 +
    aRec := Default(TRec); // initialize to default
 +
  end;
 +
 +
  procedure PrintTRec(r: PRec);
 +
  begin
 +
    WriteLn('Initialized TRec field i: ', r^.I);  // should always be zero, stack or heap
 +
  end;
 +
 +
var
 +
  a, b: PRec;
 +
 +
begin
 +
  New(a);
 +
  New(b); // standard "new" does not initialize, but now it does!
 +
  PrintTRec(a);
 +
  PrintTRec(b);
 +
  Dispose(a);
 +
  Dispose(b);
 +
end.</syntaxhighlight>
 +
 +
== Finalize ==
 +
Finalize is called when a record goes out of scope.
 +
 +
It is useful for automatic custom finalization code.
 +
<syntaxhighlight lang="pascal">
 +
program TestFinalize;
 +
 +
{$if FPC_FULLVERSION < 30101}
 +
{$ERROR this demo needs version 3.1.1}
 +
{$endif}
 +
{$mode delphi}
  
 
type
 
type
    PRec = ^TRec;
+
  PRec = ^TRec;
    TRec = record
+
 
      I : Integer;
+
  TRec = record
      class operator initialize(var aRec:TRec);
+
    I: Integer;
    end;
+
    class operator Finalize(var aRec: TRec);
 +
  end;
  
    class operator TRec.initialize(var aRec:TRec);
+
  class operator TRec.Finalize(var aRec: TRec);
    begin
+
  begin
      aRec :=Default(TRec); // initialize to default
+
    WriteLn('Just to let you know: I am finalizing..');
    end;
+
  end;
  
    procedure printTRec(r : PRec);
 
    begin
 
        WriteLn('Initialized TRec field i: ', r^.I = 0);  // should always be zero, stack or heap
 
    end;
 
 
var
 
var
a,b:PRec;  
+
  a, b: PRec;
 +
  c: array of TRec;
 +
 
 
begin
 
begin
    New(a);New(b); // standard "new" does not initialize, but now it does!
+
  New(a);
    PrintTRec(a);
+
  New(b);
    PrintTRec(b);
+
  Dispose(a);
    Dispose(a);Dispose(b);
+
  Dispose(b);
 +
  WriteLn('Just before program termination this will also be finalized');
 +
  SetLength(c, 4);
 
end.</syntaxhighlight>
 
end.</syntaxhighlight>
  
<br>
+
== AddRef ==
 +
AddRef is called after the contents of a record have been duplicated by a bitwise copy (for example '''after''', not during, an assigment.)
 +
 +
By itself it does not do any lifetime management, but you can use it to implement it. See also Copy.
 +
<syntaxhighlight lang="pascal">
 +
program TestAddref;
 +
 
 +
{$if FPC_FULLVERSION < 30101}
 +
{$ERROR this demo needs version 3.1.1}
 +
{$endif}
 +
{$mode delphi}
  
=== Finalize ===
+
uses
Finalize is called when a record goes out of scope and called before the internal call to recordrtti(data,typeinfo,@int_finalize);
+
  SysUtils;
It is useful for automatic custom finalization code. A simple example looks like:
 
<syntaxhighlight>
 
{$if FPC_FULLVERSION < 30101}{$ERROR this demo needs version 3.1.1}{$endif}
 
{$mode delphi}{$macro on}
 
program testfinalize;
 
  
 
type
 
type
    PRec = ^TRec;
+
  PRec = ^TRec;
    TRec = record
+
 
      I : Integer;
+
  TRec = record
      class operator finalize(var aRec:TRec);
+
    I: Integer;
     end;
+
    class operator AddRef(var aRec: TRec);
 +
  end;
 +
 
 +
  class operator TRec.AddRef(var aRec: TRec);
 +
  begin
 +
     WriteLn('Just to let you know: maybe you can do lifetime management here..');
 +
  end;
  
    class operator TRec.finalize(var aRec:TRec);
 
    begin
 
      writeln('Just to let you know: I am finalizing..');
 
    end;
 
 
var
 
var
a,b:PRec;
+
  a, b: array of TRec;
c:array of Trec;
+
 
 
begin
 
begin
    New(a);New(b);
+
  SetLength(a, 4);
    Dispose(a);Dispose(b);
+
  b := Copy(a);
    writeln('Just before program termination this will also be finalized');
 
    Setlength(c,4);
 
 
end.</syntaxhighlight>
 
end.</syntaxhighlight>
  
 +
== Copy ==
 +
The Copy operator, if implemented, is called instead of the default copy behavior. This operator is responsible for copying everything that's needed from the source to the target.
 +
 +
This is very helpfull if your record contains e.g. a dynamic array.<br>
 
<br>
 
<br>
 +
Consider the following program:
 +
<syntaxhighlight lang="pascal">
 +
uses
 +
  SysUtils;
  
=== AddRef ===
 
AddRef is called after the contents of a record has been duplicated by copying the contents byte by byte and is called *after* FPC internal call recordrtti(data,typeinfo,@int_addref);
 
By itself it does not any lifetime management, but you can use it to implement it. See also Copy.
 
example:
 
<syntaxhighlight>
 
{$if FPC_FULLVERSION < 30101}{$ERROR this demo needs version 3.1.1}{$endif}
 
{$mode delphi}{$macro on}
 
program testaddref;
 
uses sysutils;
 
 
type
 
type
    PRec = ^TRec;
+
  TArr = array of integer;
    TRec = record
+
  TRec = record
      I : Integer;
+
    Arr: TArr;
      class operator AddRef(var aRec:TRec);
+
  end;
    end;
 
  
    class operator TRec.Addref(var aRec:TRec);
+
function ArrToString(Arr: TArr): String;
    begin
 
      writeln('Just to let you know: maybe you can do lifetime management here..');
 
    end;
 
 
var
 
var
a,b:array of Trec;  
+
  X: Integer;
 
begin
 
begin
    setlength(a,4);
+
  Result := '';
     b:= copy(a);
+
  for X in Arr do Result := Result + IntToStr(X) + ',';
 +
  if Result <> '' then Delete(Result, Length(Result), 1);
 +
  Result := '[' + Result + ']';
 +
end;
 +
 
 +
var
 +
  R1, R2: TRec;
 +
 
 +
begin
 +
  writeln('Normal record type with a dynamic array');
 +
  R1.Arr := [1,2,3];
 +
  R2 := R1;
 +
  writeln('R1.Arr = ',ArrToString(R1.Arr));  //[1,2,3]
 +
  writeln('R2.Arr = ',ArrToString(R2.Arr));  //[1,2,3]
 +
  writeln('After R2.Arr[0] := 666');
 +
  R2.Arr[0] := 666; 
 +
  writeln('R1.Arr = ',ArrToString(R1.Arr)); //[666,2,3]
 +
  writeln('R2.Arr = ',ArrToString(R2.Arr)); //[666,2,3]
 +
end.
 +
</syntaxhighlight>
 +
This will output:
 +
<pre>
 +
Normal record type with a dynamic array
 +
R1.Arr = [1,2,3]
 +
R2.Arr = [1,2,3]
 +
After R2.Arr[0] := 666
 +
R1.Arr = [666,2,3]
 +
R2.Arr = [666,2,3]
 +
</pre>
 +
Notice that changing <tt>R2.Arr</tt> changes <tt>R1.Arr</tt> as well, since both the arrays point to the same memory location.<br>
 +
When you do <tt>R2 := R1</tt>, you do not copy the contents of R1.Arr into R2, you copy the pointer that points to the first element of R1.Arr.<br>
 +
If you want to avoid this, you could write a workaround like:
 +
 
 +
<syntaxhighlight lang="pascal">
 +
function CopyFrom(Src: TRec): TRec;
 +
begin
 +
  Result.Arr := Copy(Src.Arr);
 +
end;
 +
</syntaxhighlight>
 +
And then always use <tt>R2 := CopyFrom(R1)</tt> instead of <tt>R2 := R1</tt>.<br>
 +
<br>
 +
Or you can solve this problem using the Copy management operator:
 +
<syntaxhighlight lang="pascal">
 +
{$modeswitch advancedrecords}
 +
uses
 +
  SysUtils;
 +
 
 +
type
 +
  TManagedRec = record
 +
     Arr: TArr;
 +
    class operator Copy(constref aSrc: TManagedRec; var aDst: TManagedRec);
 +
  end;
 +
 
 +
{ TMangedRec }
 +
class operator TManagedRec.Copy(constref aSrc: TManagedRec; var aDst: TManagedRec);
 +
begin
 +
  aDst.Arr := Copy(aSrc.Arr);
 +
end;
 +
 
 +
function ArrToString(Arr: TArr): String;
 +
var
 +
  X: Integer;
 +
begin
 +
  Result := '';
 +
  for X in Arr do Result := Result + IntToStr(X) + ',';
 +
  if Result <> '' then Delete(Result, Length(Result), 1);
 +
  Result := '[' + Result + ']';
 +
end;
 +
 
 +
var
 +
  M1, M2: TManagedRec;
 +
begin
 +
  writeln('Managed record type with a dynamic array and class operator Copy');
 +
  M1.Arr := [1,2,3];
 +
  M2 := M1;
 +
  writeln('M1.Arr = ',ArrToString(M1.Arr)); //[1,2,3]
 +
  writeln('M2.Arr = ',ArrToString(M2.Arr)); //[1,2,3]
 +
  writeln('After M2.Arr[0] := 666');
 +
  M2.Arr[0] := 666;
 +
  writeln('M1.Arr = ',ArrToString(M1.Arr)); //[1,2,3]
 +
  writeln('M2.Arr = ',ArrToString(M2.Arr)); //[666,2,3]
 
end.</syntaxhighlight>
 
end.</syntaxhighlight>
 +
This will output:
 +
<pre>
 +
Managed record type with a dynamic array and class operator Copy
 +
M1.Arr = [1,2,3]
 +
M2.Arr = [1,2,3]
 +
After M2.Arr[0] := 666
 +
M1.Arr = [1,2,3]
 +
M2.Arr = [666,2,3]
 +
</pre>
  
=== Copy ===
+
There is also a (more complex) example in [https://gitlab.com/freepascal.org/fpc/source/-/blob/main/tests/test/tmoperator8.pp] within the FPC sources.
The Copy operator, if implemented, is called instead of the default copy behavior. This operator is responsible for copying everything that's needed from the source to the target.
+
 
<br>'''todo''': add simple example!
+
== Example of using Initialize and Finalize ==
There is a (complex) example in /tests/test/tmoperator8.pas
+
 
 +
<syntaxhighlight lang="pascal">
 +
unit UResourceHandlers;
 +
 
 +
{$if FPC_FULLVERSION < 30101}
 +
{$ERROR this demo needs version 3.1.1}
 +
{$endif}
 +
{$mode delphi}
 +
 
 +
interface
 +
 
 +
uses
 +
  Classes, SysUtils;
 +
 
 +
type
 +
 
 +
  { TObjectHandler }
 +
 
 +
  TObjectHandler = record
 +
    obj: TObject;
 +
    class operator Initialize(var hdl: TObjectHandler);
 +
    class operator Finalize(var hdl: TObjectHandler);
 +
  end;
 +
 
 +
implementation
 +
 
 +
{ TObjectHandler }
  
=== A Simple example of use ===
+
class operator TObjectHandler.Initialize(var hdl: TObjectHandler);
 +
begin
 +
  hdl.obj := nil;
 +
end;
  
Simple resource handler
+
class operator TObjectHandler.finalize(var hdl: TObjectHandler);
 +
begin
 +
  FreeAndNil(hdl.obj);
 +
end;
  
<syntaxhighlight>
+
end.
unit UResourseHandlers; 
 
{$if FPC_FULLVERSION < 30101}{$ERROR this demo needs version 3.1.1}{$endif} 
 
{$mode delphi}
 
{$modeSwitch advancedRecords} 
 
interface 
 
uses 
 
  Classes, SysUtils; 
 
type 
 
    { TObjectHandler } 
 
  TObjectHandler = record 
 
  obj : TObject; 
 
  class operator initialize(var hdl:TObjectHandler); 
 
  class operator finalize(var hdl:TObjectHandler); 
 
  end; 
 
implementation 
 
{ TObjectHandler } 
 
class operator TObjectHandler.initialize(var hdl: TObjectHandler); 
 
begin 
 
  hdl.obj := nil; 
 
end; 
 
class operator TObjectHandler.finalize(var hdl: TObjectHandler); 
 
begin 
 
  FreeAndNil(hdl.obj); 
 
end; 
 
end.
 
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 
How to use it
 
How to use it
  
<syntaxhighlight>
+
<syntaxhighlight lang="pascal">
procedure ExtractionResultTests.ObjectHandlerTest;
+
procedure ExtractionResultTests.ObjectHandlerTest;
var
+
var
   a: TRow;
+
   a: TRow;
   ah : TObjectHandler;
+
   ah: TObjectHandler;
begin
+
begin
  a:= TRow.Create;
+
  a := TRow.Create;
  ah.obj:= a;
+
  ah.obj := a;
end;
+
end;
 
</syntaxhighlight>
 
</syntaxhighlight>
  
In this case the Destructor of the TRow object is called when the handler goes out of scope, the same idea could be used for other resources like TMutex or TCriticalSeccions i guess.
+
In this case the destructor of the TRow object is called when the handler goes out of scope. The same idea could be used for other resources like TMutex / TCriticalSection / anything else along those lines.
  
 
[[Category:FPC]]
 
[[Category:FPC]]

Latest revision as of 18:43, 26 April 2024

Management operators feature

From Free Pascal version 3.1.1 onwards, there is a new language feature called management operators for extended or advanced records.

The new operators are: Initialize, Finalize, AddRef and Copy.

These are a fairly unique feature, and are called "management operators" because:

  • Each record, even non-managed or empty, that implements any management operators becomes a managed type.
  • They make it possible to implement new custom types with their own memory management, e.g: new string types, fast TValue implementations without hacks on the RTL, etc.

Management operators have no result type as opposed to normal operators, and work via a very simple VMT.

Thanks to this, it is possible to combine management operators with all low-level RTL functions, such as InitializeArray / FinalizeArray / etc.

Management operators can be used for many things:

  • More granularly controlling the lifetimes of simple value types / primitives
  • Implementing "nullable" value types
  • Custom ARC implementations
  • A very fast RTTI.TValue implementation
  • As a replacement for manually-called Init/Done record methods like the popular "mORMot" library uses for many types (for example in SynCommons.TSynLocker).
  • Auto init/finit for pointers/classes/simple types or anything else we have in Pascal.
  • Much more

They work correctly in all possible ways with the RTL:

  • New (Initialize).
  • Dispose (Finalize).
  • Initialize (Initialize).
  • Finalize (Finalize).
  • InitializeArray (Initialize).
  • FinalizeArray (Finalize).
  • SetLength (Initialize/Finalize).
  • Copy (AddRef).
  • RTTI.IsManaged.

Management operators, when implemented, are called implicitly at various times. For example:

  • Global variables (Initialize/Finalize).
  • Local variables (Initialize/Finalize).
  • For fields inside records, objects or classes (Initialize/Finalize).
  • Variable assignment (Copy).
  • For parameters for routines - AddRef/Finalize/none - this depends on modifiers like var / constref / const.

Initialize

The initialize operator is called directly after stack (or heap) memory allocation for a record happens.

It allows for custom automatic initialization code.

program TestInitialize;

{$if FPC_FULLVERSION < 30101}
{$ERROR this demo needs version 3.1.1}
{$endif}
{$mode delphi}

type
  PRec = ^TRec;

  TRec = record
    I: Integer;
    class operator Initialize(var aRec: TRec);
  end;

  class operator TRec.Initialize(var aRec: TRec);
  begin
    aRec := Default(TRec); // initialize to default
  end;

  procedure PrintTRec(r: PRec);
  begin
    WriteLn('Initialized TRec field i: ', r^.I);  // should always be zero, stack or heap
  end;

var
  a, b: PRec;

begin
  New(a);
  New(b); // standard "new" does not initialize, but now it does!
  PrintTRec(a);
  PrintTRec(b);
  Dispose(a);
  Dispose(b);
end.

Finalize

Finalize is called when a record goes out of scope.

It is useful for automatic custom finalization code.

program TestFinalize;

{$if FPC_FULLVERSION < 30101}
{$ERROR this demo needs version 3.1.1}
{$endif}
{$mode delphi}

type
  PRec = ^TRec;

  TRec = record
    I: Integer;
    class operator Finalize(var aRec: TRec);
  end;

  class operator TRec.Finalize(var aRec: TRec);
  begin
    WriteLn('Just to let you know: I am finalizing..');
  end;

var
  a, b: PRec;
  c: array of TRec;

begin
  New(a);
  New(b);
  Dispose(a);
  Dispose(b);
  WriteLn('Just before program termination this will also be finalized');
  SetLength(c, 4);
end.

AddRef

AddRef is called after the contents of a record have been duplicated by a bitwise copy (for example after, not during, an assigment.)

By itself it does not do any lifetime management, but you can use it to implement it. See also Copy.

program TestAddref;

{$if FPC_FULLVERSION < 30101}
{$ERROR this demo needs version 3.1.1}
{$endif}
{$mode delphi}

uses
  SysUtils;

type
  PRec = ^TRec;

  TRec = record
    I: Integer;
    class operator AddRef(var aRec: TRec);
  end;

  class operator TRec.AddRef(var aRec: TRec);
  begin
    WriteLn('Just to let you know: maybe you can do lifetime management here..');
  end;

var
  a, b: array of TRec;

begin
  SetLength(a, 4);
  b := Copy(a);
end.

Copy

The Copy operator, if implemented, is called instead of the default copy behavior. This operator is responsible for copying everything that's needed from the source to the target.

This is very helpfull if your record contains e.g. a dynamic array.

Consider the following program:

uses
  SysUtils;

type
  TArr = array of integer;
  TRec = record
    Arr: TArr;
  end;

function ArrToString(Arr: TArr): String;
var
  X: Integer;
begin
  Result := '';
  for X in Arr do Result := Result + IntToStr(X) + ',';
  if Result <> '' then Delete(Result, Length(Result), 1);
  Result := '[' + Result + ']';
end;

var
  R1, R2: TRec;

begin
  writeln('Normal record type with a dynamic array');
  R1.Arr := [1,2,3];
  R2 := R1;
  writeln('R1.Arr = ',ArrToString(R1.Arr));  //[1,2,3]
  writeln('R2.Arr = ',ArrToString(R2.Arr));  //[1,2,3]
  writeln('After R2.Arr[0] := 666');
  R2.Arr[0] := 666;  
  writeln('R1.Arr = ',ArrToString(R1.Arr)); //[666,2,3]
  writeln('R2.Arr = ',ArrToString(R2.Arr)); //[666,2,3]
end.

This will output:

Normal record type with a dynamic array
R1.Arr = [1,2,3]
R2.Arr = [1,2,3]
After R2.Arr[0] := 666
R1.Arr = [666,2,3]
R2.Arr = [666,2,3]

Notice that changing R2.Arr changes R1.Arr as well, since both the arrays point to the same memory location.
When you do R2 := R1, you do not copy the contents of R1.Arr into R2, you copy the pointer that points to the first element of R1.Arr.
If you want to avoid this, you could write a workaround like:

function CopyFrom(Src: TRec): TRec;
begin
  Result.Arr := Copy(Src.Arr);
end;

And then always use R2 := CopyFrom(R1) instead of R2 := R1.

Or you can solve this problem using the Copy management operator:

{$modeswitch advancedrecords}
uses
  SysUtils;

type
  TManagedRec = record
    Arr: TArr;
    class operator Copy(constref aSrc: TManagedRec; var aDst: TManagedRec);
  end;

{ TMangedRec }
class operator TManagedRec.Copy(constref aSrc: TManagedRec; var aDst: TManagedRec);
begin
  aDst.Arr := Copy(aSrc.Arr);
end;

function ArrToString(Arr: TArr): String;
var
  X: Integer;
begin
  Result := '';
  for X in Arr do Result := Result + IntToStr(X) + ',';
  if Result <> '' then Delete(Result, Length(Result), 1);
  Result := '[' + Result + ']';
end;

var
  M1, M2: TManagedRec;
begin
  writeln('Managed record type with a dynamic array and class operator Copy');
  M1.Arr := [1,2,3];
  M2 := M1;
  writeln('M1.Arr = ',ArrToString(M1.Arr)); //[1,2,3]
  writeln('M2.Arr = ',ArrToString(M2.Arr)); //[1,2,3]
  writeln('After M2.Arr[0] := 666');
  M2.Arr[0] := 666; 
  writeln('M1.Arr = ',ArrToString(M1.Arr)); //[1,2,3]
  writeln('M2.Arr = ',ArrToString(M2.Arr)); //[666,2,3]
end.

This will output:

Managed record type with a dynamic array and class operator Copy
M1.Arr = [1,2,3]
M2.Arr = [1,2,3]
After M2.Arr[0] := 666
M1.Arr = [1,2,3]
M2.Arr = [666,2,3]

There is also a (more complex) example in [1] within the FPC sources.

Example of using Initialize and Finalize

unit UResourceHandlers;

{$if FPC_FULLVERSION < 30101}
{$ERROR this demo needs version 3.1.1}
{$endif}
{$mode delphi}

interface

uses
  Classes, SysUtils;

type

  { TObjectHandler }

  TObjectHandler = record
    obj: TObject;
    class operator Initialize(var hdl: TObjectHandler);
    class operator Finalize(var hdl: TObjectHandler);
  end;

implementation

{ TObjectHandler }

class operator TObjectHandler.Initialize(var hdl: TObjectHandler);
begin
  hdl.obj := nil;
end;

class operator TObjectHandler.finalize(var hdl: TObjectHandler);
begin
  FreeAndNil(hdl.obj);
end;

end.

How to use it

procedure ExtractionResultTests.ObjectHandlerTest;
var
  a: TRow;
  ah: TObjectHandler;
begin
  a := TRow.Create;
  ah.obj := a;
end;

In this case the destructor of the TRow object is called when the handler goes out of scope. The same idea could be used for other resources like TMutex / TCriticalSection / anything else along those lines.