2

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.

Johan
  • 74,508
  • 24
  • 191
  • 319
  • The code that fails is remote? Can't it appear in the question? – David Heffernan Jul 06 '15 at 19:21
  • "I know Delphi does not do string folding on identical strings." What do you mean by this? The *compiler* most certainly does do string constant/literal folding. If you mean at run time, then please explain. Without some kind of globally managed (and immutable) string pool, doing this at run time seems inefficient. There are also instances where multiple copies of the same string across different units may not be folded, but that as a function of the linker, which cares not for the data content. – Allen Bauer Jul 06 '15 at 19:39
  • @alan sorry 'bout that, I meant does not do string folding at runtime. – Johan Jul 06 '15 at 20:04
  • @david, the code that fails is too big to fit into the question. – Johan Jul 06 '15 at 20:06
  • Surely you can cut it down enough. What are we to do. Hunt around for the code that doesn't work? How can we know where to look? Can't you help us out? – David Heffernan Jul 06 '15 at 20:06
  • @david, I'll try to see if I can reproduce by trimming down. Will see if I can post an update with the relevant info tomorrow. It's a complex issue. – Johan Jul 06 '15 at 20:10
  • @Johan Have you added this to QC? – John Lewis Jul 06 '15 at 21:54
  • @Allen Bauer also refers to the behavior when you assign the same string constant (even declared as a named const) to two different string variables, you can end up with two different string pointers. – Eric Grange Jul 07 '15 at 06:09
  • 1
    @Eric What's a named const? Do you mean typed const? I don't think that they behave that way. They have ref count -1 and live in read only memory and assignment is a simple ref copy. So by a named const do you mean a true const, `const s = 'foo';` – David Heffernan Jul 07 '15 at 06:18
  • @David Heffernan check UStrAsg, they are allocated anew. A named const is a constant with a name (const s = 'foo'), an unnamed one does not have a name ( a := a + 'foo' ), Delphi unifies string constants, the naming just makes the unification unambiguous. However generally speaking Delphi does not really have "true" constants (named or not), as all constants can be mutated by helpers for instance (except for string constants, which are "protected" by the UStrAsg mechanism). A typed const is when the constant type is specified explicitly, it can be named or unnamed. – Eric Grange Jul 07 '15 at 08:43
  • 1
    @Eric Are these your terms? I don't think I've seen the terms named constant and unmaned constant before. – David Heffernan Jul 07 '15 at 08:44
  • @johnlewis, this is not a effective way of communicating. And I for one do not appreciate the tone. – Johan Jul 07 '15 at 09:36
  • @Johan Sorry but I reported like 16 bugs none were fixed? There is a justified reason to be angry. – John Lewis Jul 07 '15 at 09:39
  • 1
    @David: I agree. What Eric calls a named const is usually called an untyped const, what he calls an unnamed const is usually called a literal. And AFAIK, true constants, like `const x = 7;`can not be modified at all. You could probably modify string constants with pointer tricks (although I guess that the OS won't allow it - didn't try), but most true constants do not have an address and can't be modified. – Rudy Velthuis Jul 07 '15 at 09:39
  • 1
    @Rudy You've made up those terms too so far as I can see. I think the terms are *constant expression*, *true constant* and *typed constant*. Reference: http://docwiki.embarcadero.com/RADStudio/en/Declared_Constants – David Heffernan Jul 07 '15 at 09:47
  • 1
    I did not make up anything. The terms "true constant", "typed constant" and "literal" are quite common and those are the terms I mentioned. I did not make up "named constant" (for an untyped - as opposed to typed - AKA true constant) and "unnamed constant" (for a literal), that was Eric. – Rudy Velthuis Jul 07 '15 at 10:13
  • @RudyVelthuis If you didn't make those terms up, then they would appear in the documentation. The distinction you draw between untyped const and literal cannot be found in the documentation so far as I can tell. Remember that we are discussing here the official names for these things. – David Heffernan Jul 07 '15 at 11:44
  • 1
    @RudyVelthuis a literal is a just special case of unnamed constant, and while untyped named constants are not modifiable, they are still not "true constants" as they can take different values depending on context (which makes them a classic source of bugs for floating point code) – Eric Grange Jul 07 '15 at 13:46
  • 1
    You seem to have a different vocabulary than most. And what you call "named constant" is called "true constant" in most Delphi texts.And a literal *Is* what you call "unnamed constant". In Delphi, a true constant is generally untyped, i.e. 7 can assume different values depending on the context, but you can type it, using `x = Integer(7);`. – Rudy Velthuis Jul 07 '15 at 13:59
  • Which terms did I make up? – Rudy Velthuis Jul 07 '15 at 14:00
  • An untyped const is something like `const x = 7;`. It is sometimes called like that to distinguish it from a typed const, like `const x: Integer = 7;`. The former is a true constant, the latter is not. But you can also have *typed* true constants, in the form: `const X = Integer(7);`. A literal is a constant (or a constant expression) that does not have an identifier, e.g. `7` in `X := X + 7`, or `'ABC'` in `S := S + 'ABC';`. These terms are certainly not something I made up. – Rudy Velthuis Jul 07 '15 at 14:06
  • FWIW, @David, what about [this](https://en.wikipedia.org/wiki/Literal_%28computer_programming%29)? – Rudy Velthuis Jul 07 '15 at 14:14
  • @Rudy You made up all the terms that you used that cannot be found in the documentation. – David Heffernan Jul 07 '15 at 14:21
  • That was not my question. Which terms exactly did I make up? – Rudy Velthuis Jul 07 '15 at 14:23
  • @Rudy The terms not used by the documentation. The documentation defines terminology. – David Heffernan Jul 09 '15 at 05:21
  • No, it does not. But just mention the terms that were wrong, and don't spin around it. – Rudy Velthuis Jul 09 '15 at 06:41
  • Ok, to expand a little: The docs are written by humans, who are not infallible and who have their preferences, also in terms, The Delphi docs have been written several times, and different terms have been used. And not the docs determine terminology. Terminology exists and the docs use some of it (and on a later date, with a different author, some other part of it). The terms I used have all been used in the context of Delphi and with the meaning I used. – Rudy Velthuis Jul 09 '15 at 07:03
  • @RudyVelthuis "True constant" is terribly confusing and misguided, do not use it anymore. The untyped constants are actually interpreted by the compiler either as an unnamed typed constant or like a macro ersatz, depending on the context. – Eric Grange Jul 09 '15 at 14:17
  • "True constant" is a term used and defined in the documentation and other publications, It is not confusing and misguided at all, IMO. *A true constant defines an identifier for a simple - i.e. non-compound - literal*. Since literals can have different values depending on their context (e.g. `myFloat := 7;` and `myInt := 7;` lead to totally different bit representations), true constants can do that too. – Rudy Velthuis Jul 09 '15 at 15:37

0 Answers0