Jump to content
Marco Cantu

Custom Managed Records Coming in Delphi 10.3

Recommended Posts

Awesome, but this

Quote

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.

sounds ominous. Could you please elaborate?

Share this post


Link to post

On one side EMBT removes "automatic" management for classes (ARC) on the other hand they introduce automatisms for "static" types. It might be dangerous. Is there a way to off switch this (useful) feature?

Share this post


Link to post

It would seem that by not providing constructors/destructors/assign you would get the "old" behavior.

Share this post


Link to post

I think if we use copy constructor we can create smart pointers like this:

procedure DoSomething(OtherParameters: string; New: TAutoFreeRecord); //here "New" record is created by copy constructor 
var
    s: TStrings;
begin
    s := New.Of(TStringList.Create);
    s.Add('one');
    s.Add('two');
    //here "New" goes out of scope and will destroy "s" (custom destructor)
end;

or with inline declaration:

 

procedure DoSomething(OtherParameters: string; New: TAutoFreeRecord); //here "New" record is created by copy constructor
begin
    s := New.Of(TStringList.Create);
    s.Add('one');
    s.Add('two');
    //here "New" goes out of scope and will destroy "s" (custom destructor)
end;

It was very nice if managed records in 10.3 had member lifting. In that way we could escape declaring "New"; New.Of will return instead a record which the only member could be lifted and used as a TStrings.

Share this post


Link to post
1 hour ago, Markus Kinzler said:

On one side EMBT removes "automatic" management for classes (ARC) on the other hand they introduce automatisms for "static" types. It might be dangerous. Is there a way to off switch this (useful) feature?

They don't intrduce automatisms you don't want. In fact, they now let you customize or even avoid the already existing automatisms (e.g. for records with managed content), allowing for better optimization.

 

At the moment, normal, so called plain records (i.e. not M-records) are not managed. But records with managed content are already automatically managed using the usual (hidden) functions System._InitializeRecord, System_CopyRecord and System._FinalizeRecord. That management always takes place, and there (was) no way to get out of that. But these routines are slow and use TypeInfo. They must walk through that type info to determine which members must be initialized, refcounted, finalized, etc. at what moment. That takes time.

 

With the new M-records, you can replace these routines by declaring a (default) constructor without parameters, a (default) destructor and a new Assign operator. These *replace* _InitializeRecord, _CopyRecord and _FinalizeRecord. So now you can selectively only initialize/copy/finalize what needs to be done. This saves time, as your code doesn't have to slowly walk/search the typeinfo to find what must be done for each member field. This is a possibility for manual optimization many people (including me) have asked for already. And it opens up new possibilities, like e.g. RAII. If you combine it with inline declared, block local variables, there are many possibilities.

  • Like 1
  • Thanks 1

Share this post


Link to post
18 minutes ago, Emil Mustea said:

I think if we use copy constructor we can create smart pointers like this:


procedure DoSomething(OtherParameters: string; New: TAutoFreeRecord); //here "New" record is created by copy constructor 
var
    s: TStrings;
begin
    s := New.Of(TStringList.Create);
    s.Add('one');
    s.Add('two');
    //here "New" goes out of scope and will destroy "s" (custom destructor)
end;

or with inline declaration:

 


procedure DoSomething(OtherParameters: string; New: TAutoFreeRecord); //here "New" record is created by copy constructor
begin
    s := New.Of(TStringList.Create);
    s.Add('one');
    s.Add('two');
    //here "New" goes out of scope and will destroy "s" (custom destructor)
end;

It was very nice if managed records in 10.3 had member lifting. In that way we could escape declaring "New"; New.Of will return instead a record which the only member could be lifted and used as a TStrings.

That should be

 

begin 
  var s := New.Of(TStringList.Create);

Note that Of could be an error (keyword), although as New.Of it might compile. You could give TAutoFreeRecord a class function returning an instance of the record:


 

begin
  var S := TAutoFreeRecord.New(TStringList.Create); // returns TStringList instance

 

Share this post


Link to post
31 minutes ago, Rudy Velthuis said:

That should be

 


begin 
  var s := New.Of(TStringList.Create);

Note that Of could be an error (keyword), although as New.Of it might compile. You could give TAutoFreeRecord a class function returning an instance of the record:


 


begin
  var S := TAutoFreeRecord.New(TStringList.Create); // returns TStringList instance

 

1. I get to used with inline declaration 🙂

2. You wrote "instance of a record" - in this case we need member lifting. If TAutoFreeRecord returns generic type of that parameter, then it's like the example I gave.

Share this post


Link to post
1 hour ago, Emil Mustea said:

2. You wrote "instance of a record" - in this case we need member lifting. If TAutoFreeRecord returns generic type of that parameter, then it's like the example I gave.

Wha exactly do you mean with "member lifting"? I only know the term in association with nullable types.

Share this post


Link to post
32 minutes ago, Rudy Velthuis said:

Wha exactly do you mean with "member lifting"? I only know the term in association with nullable types.

instead of writing s.Value.Add('one'), you write s.Add('one'), where "s" is a record but point to the only member of that record, which is in our case a TStringList. So you have member lifting.

Share this post


Link to post

@Rudy Velthuis I think you misunderstood me. My concern are only about the automatic invocation.

Quote

 

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);
var
  my1: TMyRecord;
begin
  Log (my1.Value.ToString);
end;

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.

 

 

Share this post


Link to post
1 hour ago, Markus Kinzler said:

@Rudy Velthuis I think you misunderstood me. My concern are only about the automatic invocation.

 

Automatic invocation is the whole point of managed records. If they have constructors and destructors, you want them to run.

Yes, you can do that manually, but then they would not be called managed records. 

 

If you don't want managed records, and you want to perform some initialization and finalization code manually, you can use plain record procedures. You don't need constructors and destructors in that case.

  • Like 2

Share this post


Link to post
19 hours ago, Markus Kinzler said:

On one side EMBT removes "automatic" management for classes (ARC) on the other hand they introduce automatisms for "static" types. It might be dangerous. Is there a way to off switch this (useful) feature?

There's nothing to switch off. You choose to switch it on on a type by type basis. 

  • Like 2

Share this post


Link to post

At today's webinar, new custom managed records in Delphi have been shown. There was such a declaration:

NewRecords.jpg.c6a9ecd3b71e2d83be18528c4d9afa37.jpg

And the implementation of operator Assign:

class operator TMyRecord.Assign(var Dest: TMyRecord; var Src: TMyRecord);
begin
  Dest.Value := Src.Value;
end;

Is there an error here? Perhaps it would be more correct to write const Src: TMyRecord? Because the above declaration allows you to change the Src variable:

  Src.Value := Dest.Value;

Even better it would be to write it as 

class operator TMyRecord.Assign(const Src: TMyRecord): TMyRecord;
begin
  Result.Values := Src.Value;  
end;

And prohibit to change the variable Result itself (it's preallocated).

Edited by Kryvich

Share this post


Link to post

@Cristian Peța Yes, but the source can be constant, or a constant parameter of a method.

procedure TestRec(const DoNotChangeMe: TManagedRecord);
begin
  ...
  var tempRec := DoNotChangeMe;
  ...
end;

 

Edited by Kryvich

Share this post


Link to post

It is still possible to change the fields of a const record parameter when you use one of its own methods to do that.

Share this post


Link to post

@Uwe Raabe True remark. It is important that the source variable itself is not changed, so it must be declared as const

Share this post


Link to post
1 hour ago, Uwe Raabe said:

It is still possible to change the fields of a const record parameter when you use one of its own methods to do that.

This is still an enormous weakness in the language. 

Share this post


Link to post
Guest
1 hour ago, Uwe Raabe said:

It is still possible to change the fields of a const record parameter when you use one of its own methods to do that.

So you would say this code

type
  TTestRec = record
    Value: Integer;
    procedure Change( AValue: Integer );
  end;

  { TTestRec }

procedure TTestRec.Change( AValue: Integer );
begin
  Self.Value := AValue;
end;

procedure Test1( const rec: TTestRec );
begin
  // will not compile
  // rec.Value := 1;
end;

procedure Test2( const rec: TTestRec );
begin
  rec.Change( 2 );
end;

procedure Test3( rec: TTestRec );
begin
  rec.Value := 3;
end;

procedure Test4( rec: TTestRec );
begin
  rec.Change( 4 );
end;

procedure Test5( var rec: TTestRec );
begin
  rec.Value := 5;
end;

procedure Test6( var rec: TTestRec );
begin
  rec.Change( 6 );
end;

procedure Main;
var
  rec: TTestRec;
begin
  rec.Value := 0;
  Test1( rec );
  Writeln( rec.Value );
  Test2( rec );
  Writeln( rec.Value );
  Test3( rec );
  Writeln( rec.Value );
  Test4( rec );
  Writeln( rec.Value );
  Test5( rec );
  Writeln( rec.Value );
  Test6( rec );
  Writeln( rec.Value );
end;

begin
  try
    Main( );
  except
    on E: Exception do
      Writeln( E.ClassName, ': ', E.Message );
  end;
  Readln;

end.

should produce the following result?

0
2 <-- const record modified by method
0
0
5
6

Well, actually I get

0
0
0
0
5
6

I can only change the var records.

Edited by Guest

Share this post


Link to post

Another variation

type
  PTestRec = ^TTestRec;

procedure Test7( const rec: PTestRec );
begin
  rec.Change( 7 );
end;

...

  Test7( @rec );
  Writeln( rec.Value );

shows 7

Share this post


Link to post
Guest

@Lars Fosdal With a const argument you cannot change the argument itself. The argument in this case is the Pointer and not the record. This is nearly the same as passing the record as var argument.

procedure foo( arg ) // we can change arg, but it is a copy
procedure foo( const arg ) // we cannot change arg
procedure foo( var arg ) // we can change arg

But you should always be aware what arg really is (f.i. reference type it is the reference)

Edited by Guest

Share this post


Link to post

True.  My point was that relying on the const keyword as "security against tampering" is - as David also points out - a bit of a gamble.

Share this post


Link to post
Guest
2 minutes ago, Lars Fosdal said:

True.  My point was that relying on the const keyword as "security against tampering" is - as David also points out - a bit of a gamble.

Well, if you pass a pointer to memory from your own process you should not be surprised that you can modify the memory you are pointing at. I would be surprised if I could not.

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×