Jump to content
havrlisan

Possible compiler bug with generics?

Recommended Posts

I've encountered a bug with generics which I believe is a mistake in the compiler. I'll try to be as detailed as possible throughout the code below. I'm only interested in opinions on what is causing this issue in the low-level code, and on possible workarounds that are not entirely different from the implementation I intended to do here. Note that this is a bare minimum of code implementation necessary to reproduce the bug.

 

TL;DR skip to HERE to see where the bug occurs.

program GenericsBugConsole;

{$APPTYPE CONSOLE}
{$R *.res}

uses
  System.Classes,
  System.SysUtils,
  System.Generics.Collections;

type
  //  Simple child class and interface inheriting from TInterfacedObject.
  //  Contains one property necessary to demonstrate the bug (by invokation).
  IAncestorObject = interface
    ['{8B57E255-F48D-4982-B9AF-71A6ABBCDBA4}']
    function GetID: TGUID;
    property ID: TGUID read GetID;
  end;

  TAncestorObject = class(TInterfacedObject, IAncestorObject)
  protected
    function GetID: TGUID;
  end;

  //  Generic list that acts as a wrapper for the generic TList<T>.
  //  Implementation is bare minimum for demonstration purpose.
  IGenericList<T: IInterface> = interface
    ['{B449EFCE-527D-4062-924E-A0E0421B8A16}']
    procedure Add(const AObj: T);
    function First: T;
  end;

  TGenericList<T: IInterface> = class(TInterfacedObject, IGenericList<T>)
  private
    FList: TList<T>;
  protected
    procedure Add(const AObj: T);
    function First: T;
  public
    constructor Create;
    destructor Destroy; override;
  end;

// Classes implementation
  { TAncestorObject }

function TAncestorObject.GetID: TGUID;
begin
  // irrelevant
  Result := TGUID.NewGuid;
end;

{ TGenericList<T> }

constructor TGenericList<T>.Create;
begin
  FList := TList<T>.Create;
end;

destructor TGenericList<T>.Destroy;
begin
  FList.Free;
  inherited;
end;

procedure TGenericList<T>.Add(const AObj: T);
begin
  FList.Add(AObj);
end;

function TGenericList<T>.First: T;
begin
  Result := FList.First;
end;

begin
  try
    var
      LList: IGenericList<IAncestorObject> := TGenericList<IAncestorObject>.Create;

    //  HERE:
    //  This is the place where the compiler starts acting weird. The call to Supports() passes,
    //  and the debugger correctly shows that TCastedList is "TGenericList<IAncestorObject> as IGenericList<IInterface>".
    //  Calling LCastedList.Add() also passes, TAncestorObject.Create will be casted to IInterface (as per arg type).
    var
      LCastedList: IGenericList<IInterface>;
    if Supports(LList, IGenericList<IInterface>, LCastedList) then
      LCastedList.Add(TAncestorObject.Create);

    //  LObj is fetched from LList (IGenericList<IAncestorObject>),
    //  and the debugger also shows that correctly: "TAncestorObject as IAncestorObject".
    var
    LObj := LList.First;

    //  This is where the exception occurs. Throughout 3 different projects, I've received three different exceptions:
    //  1) 'c0000005 ACCESS_VIOLATION' -> Private console app, +300000 lines of code
    //  2) 'access violation at 0x00000001: access of address 0x00000001' -> Test FMX app, bare minimum code with TForm
    //  3) 'Privileged instruction' -> Not sure where I got this one, and I cannot get it again. Possibly on another test console app.
    var
    LID := LObj.ID;
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;

end.

 

Edited by havrlisan
Code syntax, Pascal does not recognize multiline comments

Share this post


Link to post

I forgot to mention a very important information:

If I add the object as IAncestorObject to LCastedList (which expects IInterface), the bug does not happen.

 

And if I add the object as IAncestorObject to the LList (which expects IAncestorObject), the bug also does not happen.

Share this post


Link to post

There is not bug in compiler, but in your code.

 

When you say 

LCastedList.Add(TAncestorObject.Create)

What will happen is equivalent of following code:

 

var Tmp: IInterface := TAncestorObject.Create;
LCastedList.Add(Tmp);

This will be because LCastedList is declared as IGenericList<IInterface> and stores IInterface references.

 

However when you retrieve stored reference you will call that on original list which is declared as IGenericList<IAncestorObject> and retrieved reference will be of type IAncestorObject and you can try to call LObj.ID on that retrieved reference. But in reality stored reference was IInterface, not IAncestorObject and it crashes when you call ID on it because that method (GetID) it simply does not exist there.

 

Point of generics is that you have same type stored in the background, not to store incompatible types. even though there is same object behind those two interface references, those interfaces are completely different types represent different entry points to that object. Sometimes you can force "wrong" type to be stored at runtime, which is usually not what you should be doing, but if you do, you need to make sure that every time you retrieve something that might be "wrong" you cast it back to appropriate type.

 

    var
      LTmp := LList.First;
    var
      LObj: IAncestorObject;
    if Supports(LTmp, IAncestorObject, LObj) then
       var LID := LObj.ID ;

 

  • Like 1
  • Thanks 1

Share this post


Link to post
14 minutes ago, Dalija Prasnikar said:

There is not bug in compiler, but in your code.

 

When you say 


LCastedList.Add(TAncestorObject.Create)

What will happen is equivalent of following code:

 


var Tmp: IInterface := TAncestorObject.Create;
LCastedList.Add(Tmp);

This will be because LCastedList is declared as IGenericList<IInterface> and stores IInterface references.

 

However when you retrieve stored reference you will call that on original list which is declared as IGenericList<IAncestorObject> and retrieved reference will be of type IAncestorObject and you can try to call LObj.ID on that retrieved reference. But in reality stored reference was IInterface, not IAncestorObject and it crashes when you call ID on it because that method (GetID) it simply does not exist there.

 

Point of generics is that you have same type stored in the background, not to store incompatible types. even though there is same object behind those two interface references, those interfaces are completely different types represent different entry points to that object. Sometimes you can force "wrong" type to be stored at runtime, which is usually not what you should be doing, but if you do, you need to make sure that every time you retrieve something that might be "wrong" you cast it back to appropriate type.

 


    var
      LTmp := LList.First;
    var
      LObj: IAncestorObject;
    if Supports(LTmp, IAncestorObject, LObj) then
       var LID := LObj.ID ;

 

You're right, thank you. I did think that was the issue as you've written, but I thought it would be implicitly cast into IAncestorObject when either Added to TList<T> or when retrieved via First().

Share this post


Link to post
4 minutes ago, havrlisan said:

You're right, thank you. I did think that was the issue as you've written, but I thought it would be implicitly cast into IAncestorObject when either Added to TList<T> or when retrieved via First().

The problem is that all variants of IGenericList<T> have same GUID so Supports function will succeed even though those different generic lists are not compatible.

  • Like 1

Share this post


Link to post
On 6/9/2023 at 3:47 PM, Dalija Prasnikar said:

The problem is that all variants of IGenericList<T> have same GUID so Supports function will succeed even though those different generic lists are not compatible.

Come to think of it, a compiler directive for raising errors in such scenarios would be an awesome option. Something like overflow and range checking compiler directives.

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

×