Search code examples
delphidelphi-xe7delphi-xe8

Can I compare Real48 using generics.defaults?


The following code to compare two Real48's (6-byte float) compiles and runs, but either generates non-nonsensical results or generates a AV.

program Project44;

{$APPTYPE CONSOLE}
uses
  System.SysUtils,
  System.Generics.Defaults;

begin
  try
    WriteLn(System.Generics.Defaults.TComparer<Real48>.Default.Compare(100.0,100.0));
    WriteLn('all ok, press space');
  except on E:exception do
    WriteLn(e.Message);
  end;
  ReadLn
end.

It should output 0, but if it does not bomb first it outputs -92 or some other incorrect value.

Is this bug still present in the lastest XE8?
And if so, has it been reported before, I cannot find anything on the https://quality.embarcadero.com, but if there's an older QC I would like to refer to that.

Finally.... How do I compare two REAL48 types using TComparer<something>?

EDIT :
this was the fix I settled upon:

interface
...snip...
[Test]
procedure TestReal48;
...snip...  
    TTest<T> = record
  private
    class var Def: System.Generics.Defaults.IComparer<T>;
    class var F: FastDefaults.TComparison<T>;
  public
    class function Real48Comparison(const Left, Right: T): Integer; static;

implementation

procedure TestDefault.TestReal48;
var
  OldDef: System.Generics.Defaults.IComparer<Real48>;
begin
  OldDef:= TTest<Real48>.Def;
  TTest<Real48>.Def:= System.Generics.Defaults.TComparer<Real48>.Construct(TTest<Real48>.Real48Comparison);
  TTest<Real48>.Test(100.0,100.0);
  TTest<Real48>.Test(100000.0,-10000.0);
  TTest<Real48>.Test(0.0,-10000.0);
  TTest<Real48>.Test(100000.0,0.0);
  TTest<Real48>.Test(0.0,0.0);
  TTest<Real48>.Def:= OldDef;
end;

Solution

  • This defect is present in all versions of the compiler. Since Real48 was deprecated more than a decade ago I would expect that Embarcadero would not change the behaviour, even if you submitted a bug report. Of course, you should still submit a bug report, but I would not hold your breath when waiting for a fix!

    You'll have to construct a comparer rather than relying on the default:

    var
      Comparer: IComparer<Real48>;
    
    function Real48Comparison(const Left, Right: Real48): Integer;
    begin
      if Left < Right then
        Result := -1
      else if Left > Right then
        Result := 1
      else
        Result := 0;
    end;
    
    Comparer := System.Generics.Defaults.TComparer<Real48>.Construct(Real48Comparison);
    

    Why does the default Real48 comparer fail so hard. Well, it starts here:

    class function TComparer<T>.Default: IComparer<T>;
    begin
      Result := IComparer<T>(_LookupVtableInfo(giComparer, TypeInfo(T), SizeOf(T)));
    end;
    

    It transpires that TypeInfo(Real48) yields nil. There would appear to be no type info available for Real48. Probably not a great surprise.

    Then we reach here:

    function _LookupVtableInfo(intf: TDefaultGenericInterface; info: PTypeInfo; size: Integer): Pointer;
    var
      pinfo: PVtableInfo;
    begin
      if info <> nil then
      begin
        pinfo := @VtableInfo[intf, info^.Kind];
        Result := pinfo^.Data;
        if ifSelector in pinfo^.Flags then
          Result := TTypeInfoSelector(Result)(info, size);
        if ifVariableSize in pinfo^.Flags then
          Result := MakeInstance(Result, size);
      end
      else
      begin
        case intf of
          giComparer: Result := Comparer_Selector_Binary(info, size);
          giEqualityComparer: Result := EqualityComparer_Selector_Binary(info, size);
        else
          System.Error(reRangeError);
          Result := nil;
        end;
      end;
    end;
    

    We take the else branch and call Comparer_Selector_Binary. So we end up performing a binary comparison. The comparison is actually performed by this function:

    function Compare_Binary(Inst: PSimpleInstance; const Left, Right): Integer;
    begin
      Result := BinaryCompare(@Left, @Right, Inst^.Size);
    end;
    

    which calls:

    function BinaryCompare(const Left, Right: Pointer; Size: Integer): Integer;
    var
      pl, pr: PByte;
      len: Integer;
    begin
      pl := Left;
      pr := Right;
      len := Size;
      while len > 0 do
      begin
        Result := pl^ - pr^;
        if Result <> 0 then
          Exit;
        Dec(len);
        Inc(pl);
        Inc(pr);
      end;
      Result := 0;
    end;
    

    Not going to be useful for a real valued type.

    As for the runtime error that relates to the ABI for Real48. It seems that Real48 parameters are always passed on the stack. That is just not compatible with the use of untyped parameters in Compare_Binary.