When comparing two managed records using a inline generic class function with const parameters CopyRecord gets called on the records. This causes the pointers inside the records to change and two (previously) equal records to not test equal.
However..... When creating a simple MVCE I'm unable to reproduce the behavior.
The following code works just fine:
program RTLTestManagedRecords;
{$APPTYPE CONSOLE}
{$R *.res}
uses
System.SysUtils;
type
TCompare<T> = class
class function Compare(const Left, Right: T): integer; static; inline;
end;
TTest<T> = class
private
class var F: TCompare<T>;
public
class function Fast(const Left, Right: T): integer; static;
class procedure Test(const Left, Right: T; const message: string = ''); static;
end;
function BinaryCompare(const Left, Right; Size: NativeInt): integer; forward;
class function TCompare<T>.Compare(const Left, Right: T): integer;
begin
if GetTypeKind(T) = tkRecord then case SizeOf(T) of
0: Result:= 0;
1: Result:= (PByte(@Left)^)- (PByte(@Right)^);
2: Result:= (PWord(@Left)^)- (PWord(@Right)^);
4: Result:= (integer(PCardinal(@Left)^> PCardinal(@Right)^)-
integer(PCardinal(@Left)^< PCardinal(@Right)^));
else Result:= BinaryCompare(Left, Right, SizeOf(T));
end;
end;
{pointermath on}
function BinaryCompare(const Left, Right; Size: NativeInt): integer;
var
i: integer;
L,R: PByte;
begin
L:=@Left;
R:=@Right;
for i:= 0 to Size - 1 do begin
if L[i] <> R[i] then exit(L[i] - R[i]);
end;
Result:= 0;
end;
{$pointermath off}
type
TManagedRec = record
a: integer;
b: string;
end;
var
L,R: TManagedRec;
{ TTest<T> }
class function TTest<T>.Fast(const Left, Right: T): integer;
begin
Result:= F.Compare(Left, Right);
end;
class procedure TTest<T>.Test(const Left, Right: T; const message: string);
begin
try
WriteLn(Format(message,[TTest<T>.Fast(Left,Right)]));
except
WriteLn('Oops');
end;
end;
begin
L.a:= 1;
R.a:= 2;
L.b:= '7878787';
R.b:= '7777777';
TTest<TManagedRec>.Test(L,L,'Compare(L,L) = %d');
WriteLn(Format('Compare(L,R) = %d',[TCompare<TManagedRec>.Compare(L,R)]));
WriteLn(Format('Compare(R,L) = %d',[TCompare<TManagedRec>.Compare(R,L)]));
WriteLn(Format('Compare(R,R) = %d',[TCompare<TManagedRec>.Compare(R,R)]));
WriteLn(Format('Compare(L,L) = %d',[TCompare<TManagedRec>.Compare(L,L)]));
ReadLn;
end.
When I run it inside the more complex fastdefaults code it fails because CopyRecord gets called, even though the parameters are declared as const. Declaring them as const [ref] does not fix the issue. Removing the inline fixes it, but that removes much of the benefit of the compare function.
Question
Is anyone able to change/expand the above MVCE so that it bombs just like the larger example does?"
I'm trying to understand what is the actual trigger for the problem.
Please do not comment on the folly of comparing managed records
I know Delphi does not do string folding on identical strings at runtime.
And please do not suggest workarounds for the problem. I know the workarounds already.
TCompare<T> = class
class function Compare(const Left, Right: T): integer; static;
Note that the TCompare<T>
class is generic so it should be able to compare anything. Including managed records. The compiler should always honor const
and
TCompare<T>.Compare(ManagedRec1, ManagedRec1)
should always return 0
And I understand that
TCompare<T>.Compare(AManagedRec, SameManagedRec)
does not always return 0
and I'm fine with that.
What I'm not fine with is Delphi calling _CopyRecord
on const parameters.