Jump to content

Advantages of record constructor over record class function, reviewed after CustomRecords

Recommended Posts

Posted (edited)

Hi there,


I'm trying to make the right choice for above, and tried some simple function.

I also have seen a nice explanation about that from David


When comparing both options, it seems to have only minor differences, see below. (Rx10.4.2)

What I asked myself if there is any advantage of the record constructor over class function anyway,
also in regard of the more modern CustomRecords ?


    TRecConstructor = record
        FValue : Integer;

        constructor Create( const AValue : Integer );

    TRecClassFunc = record
        FValue : Integer;

        class function Create( const AValue : Integer ) : TRecClassFunc;  static;

procedure TS4DunitX_Basic__Tests.Test01;
    LRecCon : TRecConstructor;
    LRecCls : TRecClassFunc;

    LRecCon := TRecConstructor.Create( 42 );
    LRecCls := TRecClassFunc.Create(   84 );

{ TRecConstructor }

constructor TRecConstructor.Create(const AValue: Integer);
    FValue := AValue;

{ TRecClassFunc }

class function TRecClassFunc.Create(const AValue: Integer): TRecClassFunc;
    Result.FValue := AValue;

S4.DUnitX.Basic.__Tests.pas.77: LRecCon := TRecConstructor.Create( 42 );
00F968F5 8D45F8           lea eax,[ebp-$08]
00F968F8 BA2A000000       mov edx,$0000002a
00F968FD E832000000       call TRecConstructor.Create

S4.DUnitX.Basic.__Tests.pas.78: LRecCls := TRecClassFunc.Create(   84 );
00F96902 B854000000       mov eax,$00000054          //<== Differs
00F96907 E844000000       call TRecClassFunc.Create
00F9690C 8945F4           mov [ebp-$0c],eax

S4.DUnitX.Basic.__Tests.pas.84: end;
00F9690F 8BE5             mov esp,ebp
00F96911 5D               pop ebp
00F96912 C3               ret

S4.DUnitX.Basic.__Tests.pas.90: begin  TRecConstructor.Create
00F96934 55               push ebp
00F96935 8BEC             mov ebp,esp
00F96937 83C4F8           add esp,-$08
00F9693A 8955F8           mov [ebp-$08],edx
00F9693D 8945FC           mov [ebp-$04],eax        //<== Additional
S4.DUnitX.Basic.__Tests.pas.91: FValue := AValue;
00F96940 8B45FC           mov eax,[ebp-$04]
00F96943 8B55F8           mov edx,[ebp-$08]
00F96946 8910             mov [eax],edx
S4.DUnitX.Basic.__Tests.pas.92: end;
00F96948 8B45FC           mov eax,[ebp-$04]
00F9694B 59               pop ecx
00F9694C 59               pop ecx
00F9694D 5D               pop ebp
00F9694E C3               ret

00F9694F 90               nop

S4.DUnitX.Basic.__Tests.pas.98: begin TRecClassFunc.Create
00F96950 55               push ebp
00F96951 8BEC             mov ebp,esp
00F96953 83C4F8           add esp,-$08
00F96956 8945FC           mov [ebp-$04],eax
S4.DUnitX.Basic.__Tests.pas.99: Result.FValue := AValue;
00F96959 8B45FC           mov eax,[ebp-$04]
00F9695C 8945F8           mov [ebp-$08],eax              //<== Differs
S4.DUnitX.Basic.__Tests.pas.100: end;
00F9695F 8B45F8           mov eax,[ebp-$08]
00F96962 59               pop ecx
00F96963 59               pop ecx
00F96964 5D               pop ebp
00F96965 C3               ret


Edited by Rollo62

Share this post

Link to post

Looking at asm without $O+ is pointless.


Here is the code from both options with $O+:


RecordCtorVsClassFunc.dpr.26: begin
00408F94 51               push ecx
RecordCtorVsClassFunc.dpr.27: LRecCon := TRecConstructor.Create( 42 );
00408F95 8BC4             mov eax,esp
00408F97 BA2A000000       mov edx,$0000002a
00408F9C E813000000       call TRecConstructor.Create
RecordCtorVsClassFunc.dpr.28: end;
00408FA1 5A               pop edx
00408FA2 C3               ret 
00408FA3 90               nop 
RecordCtorVsClassFunc.dpr.33: begin
00408FA4 51               push ecx
RecordCtorVsClassFunc.dpr.34: LRecCls := TRecClassFunc.Create(   84 );
00408FA5 B854000000       mov eax,$00000054
00408FAA E809000000       call TRecClassFunc.Create
00408FAF 890424           mov [esp],eax
RecordCtorVsClassFunc.dpr.35: end;
00408FB2 5A               pop edx
00408FB3 C3               ret 

Because you have a record here that fits into a register and does not have any managed type fields it will simply be returned via eax when using a function. When using a ctor it passes its address via eax.


Now let's add a second Integer field to both records and look again:


RecordCtorVsClassFunc.dpr.26: begin
00408F94 83C4F8           add esp,-$08
RecordCtorVsClassFunc.dpr.27: LRecCon := TRecConstructor.Create( 42 );
00408F97 8BC4             mov eax,esp
00408F99 BA2A000000       mov edx,$0000002a
00408F9E E819000000       call TRecConstructor.Create
RecordCtorVsClassFunc.dpr.28: end;
00408FA3 59               pop ecx
00408FA4 5A               pop edx
00408FA5 C3               ret 
00408FA6 8BC0             mov eax,eax
RecordCtorVsClassFunc.dpr.33: begin
00408FA8 83C4F8           add esp,-$08
RecordCtorVsClassFunc.dpr.34: LRecCls := TRecClassFunc.Create(   84 );
00408FAB 8BD4             mov edx,esp
00408FAD B854000000       mov eax,$00000054
00408FB2 E809000000       call TRecClassFunc.Create
RecordCtorVsClassFunc.dpr.35: end;
00408FB7 59               pop ecx
00408FB8 5A               pop edx
00408FB9 C3               ret 

Whooop, no difference.


Now let's add another field - of type string - I stripped the prologue and epilogue from the asm shown to reduce the noise:


RecordCtorVsClassFunc.dpr.28: begin
RecordCtorVsClassFunc.dpr.29: LRecCon := TRecConstructor.Create( 42 );
004099B1 8D45E8           lea eax,[ebp-$18]
004099B4 BA2A000000       mov edx,$0000002a
004099B9 E89A000000       call TRecConstructor.Create
004099BE 8D55E8           lea edx,[ebp-$18]
004099C1 8D45F4           lea eax,[ebp-$0c]
004099C4 8B0DD0984000     mov ecx,[$004098d0]
004099CA E871D2FFFF       call @CopyRecord
RecordCtorVsClassFunc.dpr.30: end;
RecordCtorVsClassFunc.dpr.35: begin
RecordCtorVsClassFunc.dpr.36: LRecCls := TRecClassFunc.Create(   84 );
00409A22 8D55F4           lea edx,[ebp-$0c]
00409A25 B854000000       mov eax,$00000054
00409A2A E82D000000       call TRecClassFunc.Create
RecordCtorVsClassFunc.dpr.37: end;

Now we see a missing optimization when using the ctor opposed to the function - as you might know functions returning a managed type (such as a record with at least one field of a managed type such as string) are actually passed as var param (last parameter after all others, thus edx here).

The ctor code however uses an unnecessary temp copy.

Share this post

Link to post

I honestly don't think that the issues discussed in the answers so far should be a primary factor in your choices. 

  • Like 1

Share this post

Link to post

Ok, I will leave the class functions then as-is, if this makes no practical difference.

Concerning the "constructor" naming, has a somewhat different meaning IMHO, not so well fitting to records.

I will stay with the naming convention "class function TMyRecord.Make(...) : TMyRecord; static;" instead of construtors then,

to be sure not to mix both concepts.

Share this post

Link to post

Delphi developers usually have the association of allocating memory when they read constructor - and thus think that for a value type it is wrong to have one.

C++ has struct ctors and nobody (that I know) thinks they are wrong - because usually, C++ developers don't immediately think of memory allocation when they see a ctor.

Share this post

Link to post

Beside memory, I think of "constructor" Create as the necessary 1st step to setup an item correctly, before I can make use of anything from this item.

Thats not the case for records, I could work without a "creation", that why I dislike the "Create" nomenclature for records and look for something that points out the difference.

(and of course, I could name constructors anything else, but that makes it even more fuzzy).

Share this post

Link to post
21 hours ago, Stefan Glienke said:

Delphi developers usually have the association of allocating memory when they read constructor - and thus think that for a value type it is wrong to have one.

C++ has struct ctors and nobody (that I know) thinks they are wrong - because usually, C++ developers don't immediately think of memory allocation when they see a ctor.

That's why I still think the old object model did it right with explicit memory allocation separate from the constructor:


MyInstPtr := new(TSomeObjectType, Init(Parameters, go, here));






(I hope I remembered the syntax correctly. It has been over a decade that I used this.)

  • Like 1

Share this post

Link to post

Exactly - this one of the biggest gripes I have with Delphi/Pascal - the two separate concerns of allocating memory and initialization are mixed together. This is also what prevents having stack objects.

C++ for example does it differently using new to allocate memory which also calls the ctor but they are not mixed - you can also just use an object on the stack or a vector/array of objects without memory indirection.

  • Like 1

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