Rollo62 536 Posted June 9, 2021 (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 ? type TRecConstructor = record private FValue : Integer; constructor Create( const AValue : Integer ); end; TRecClassFunc = record private FValue : Integer; class function Create( const AValue : Integer ) : TRecClassFunc; static; end; procedure TS4DunitX_Basic__Tests.Test01; var LRecCon : TRecConstructor; LRecCls : TRecClassFunc; begin LRecCon := TRecConstructor.Create( 42 ); LRecCls := TRecClassFunc.Create( 84 ); end; { TRecConstructor } constructor TRecConstructor.Create(const AValue: Integer); begin FValue := AValue; end; { TRecClassFunc } class function TRecClassFunc.Create(const AValue: Integer): TRecClassFunc; begin Result.FValue := AValue; end; Quote 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 June 9, 2021 by Rollo62 Share this post Link to post
Stefan Glienke 2002 Posted June 9, 2021 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
Stefan Glienke 2002 Posted June 9, 2021 In fact, its a regression that happened just recently: https://quality.embarcadero.com/browse/RSP-33455 sometime in 10.4.x Share this post Link to post
David Heffernan 2345 Posted June 9, 2021 I honestly don't think that the issues discussed in the answers so far should be a primary factor in your choices. 1 Share this post Link to post
Rollo62 536 Posted June 10, 2021 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
Stefan Glienke 2002 Posted June 10, 2021 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
Rollo62 536 Posted June 10, 2021 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
dummzeuch 1505 Posted June 11, 2021 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)); vs. MyInst.Init(...); (I hope I remembered the syntax correctly. It has been over a decade that I used this.) 1 Share this post Link to post
Stefan Glienke 2002 Posted June 11, 2021 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. 1 Share this post Link to post