Jump to content
yonojoy

Generics: Delphi does not always seem to force the instantiated type

Recommended Posts

Consider the following code:

 

type

    IBaseBroker = interface
    ['{FE71BBA5-A6F8-4E75-894E-54F830023E99}']
    end;

    IFooBroker = interface(IBaseBroker)
        function GetMsg(): string;
    end;

    TIBase<I: IBaseBroker> = class
    private
        FBroker: I;
    protected
        property Broker: I read FBroker;
    public
        constructor Create(ABroker: I); virtual;
    end;

    TIBaseExt<I: IBaseBroker> = class (TIBase<I>)
    public
        constructor Create(ABroker: I); override;
    end;

    TIFoo = class (TIBaseExt<IFooBroker>)
    public
        constructor Create(ABroker: IFooBroker); override;
        procedure Test;
    end;

    TIBaseBroker = class(TInterfacedObject, IBaseBroker)
    end;

    TIFooBroker = class(TIBaseBroker, IFooBroker)  //no AV with ,IBaseBroker!
    protected
        function GetMsg(): string;
    end;


implementation

procedure TForm1.Button1Click(Sender: TObject);
var
    Broker: IFooBroker;
    Foo: TIFoo;
begin
    Broker := TIFooBroker.Create();
    Foo := TIFoo.Create(Broker);
    try
    	//AV here:
        Foo.Test;
    finally
        Foo.Free;
    end;
end;

constructor TIFoo.Create(ABroker: IFooBroker);
begin
    inherited Create(ABroker);
end;

procedure TIFoo.Test;
begin
    //Access violation:
    ShowMessage(Broker.GetMsg());
end;

constructor TIBase<I>.Create(ABroker: I);
begin
    FBroker := ABroker;
end;

constructor TIBaseExt<I>.Create(ABroker: I);
var
    BaseBroker: IBaseBroker;
begin
    //silly cast:
    if not Supports(ABroker, IBaseBroker, BaseBroker) then
      raise Exception.Create('...');
    // I thought the following line should not compile for TIFoo = TIBaseExt<IFooBroker>:
    inherited Create(BaseBroker);
end;

function TIFooBroker.GetMsg: string;
begin
    Result := 'OK';
end;

I thought Create(I: IBaseBroker) should not compile, once I is instantiated with IFooBroker. But it compiles and I get an AV (at least with XE4). Or do I miss something?

Edited by yonojoy

Share this post


Link to post

I can't really follow your example but I noticed that you haven't assigned IFooBroker a GUID. Is that by design?

Also, are you aware that inheriting one interface from another is just a convenience that copies the declaration from the base interface into the derived interface. There's no "interface polymorphism".

Share this post


Link to post

Why are you casting it? It makes no sense.

In a TIFoo, FBroker is already of type IFooBroker.

 

Your constructor of TIBaseExt<I> can just look like this

constructor TIBaseExt<I>.Create(ABroker: I);
begin
	inherited Create(ABroker);
end;

and therefore is not necessary at all. After removing the unnecessary casting, your code runs fine with no further changes.

 

 

Share this post


Link to post

I know the cast is senseless. This was my try to break down a real world bug to, what I think, might be a compiler bug.

 

Essentially I have a class definition with TIBaseExt<I: IBaseBroker>.Create(ABroker: I) and I can call this Create constructor with IBaseBroker even if I is instantiated with IFooBroker. In the end I should have TIBaseExt<IFooBroker>.Create(ABroker: IFooBroker) and I can call this with an IBaseBroker interface and the compiler wont complain.

 

The cast was the bug in our code and I would have expected the compiler to error on it, but instead it compiled and I had to track the access violations.

I modified the calling code to make this more clear:

procedure TForm1.Button1Click(Sender: TObject);
var
    BaseBroker: IBaseBroker;
    Broker: IFooBroker;
    Foo: TIFoo;
begin
    Broker := TIFooBroker.Create();

    if not Supports(Broker, IBaseBroker, BaseBroker) then
      raise Exception.Create('...');
    //Here the compiler will complain: E2010: Incompatible types 'IFooBroker' and 'IBaseBroker'
    TIBaseExt<IFooBroker>.Create(BaseBroker);

    //This Create will lead to the same call, but the compiler wont complain:
    Foo := TIFoo.Create(Broker);
    try
        Foo.Test;
    finally
        Foo.Free;
    end;
end;

@Anders Melander A GUID for IFooBroker wont change anything. IFooBroker and IBaseBroker are different interfaces and the compiler should IMO not allow a call with IBaseBroker if I is instantiated with IFooBroker.

 

Share this post


Link to post
13 minutes ago, yonojoy said:

IFooBroker and IBaseBroker are different interfaces

Yes, exactly. I just wanted to be sure that you were aware of this. Many people misunderstand what interface inheritance is.

 

I still can't follow your example but from your description it does sound like there's a bug. Since you're not doing any hard type casts the compiler should see the problem (whatever it is) and not produce code that leads to an AV.

Share this post


Link to post
2 hours ago, Anders Melander said:

I still can't follow your example ...

That was the most simple example I could find, that triggers the problem.

 

I have a class hierarchy:
TIBase<I>
TIBaseExt<I> = class(TIBase<I>)
TIFoo = class(TIBaseExt<IFooBroker>)

 

TIBase<I> has a local variable Broker of type I. With my code one can assign a variable of type IBaseBroker to Broker even if Broker is of type IFooBroker (via TIBase<IFooBroker>).

 

What I really wanted to know is, if I should report this to QP, especially if this problem still exists in actual Delphi versions (I only have XE4 available here).

Share this post


Link to post
59 minutes ago, yonojoy said:

With my code one can assign a variable of type IBaseBroker to Broker even if Broker is of type IFooBroker

IMO that's a bug since the two interfaces are distinct. I don't have time to investigate if the problem exists in newer versions right now.

Share this post


Link to post

Put a breakpoint on your

constructor TIBase<I>.Create(ABroker: I);
begin
    FBroker := ABroker;
end;

ABroker is of type IFooBroker. Which absolutely makes sense, because for TIFooI is of type IFooBroker.

However, you stuffed a reference of type IBaseBroker in there.

 

The compiler shouldn't have let you.

Share this post


Link to post

Note, that the implementation of TIBase<I> can be in another unit, where neither IFooBroker is known, nor that TIFoo is a type instance for IFooBroker. Nevertheless that unit has to be compiled before the unit containing IFooBroker and TIFoo.

 

The compiler should only allow anything of type I as a parameter for inherited Create. That will rule out to grab any interfaces via Supports or AS constructs. Please file a bug report.

 

Share this post


Link to post

The supports is misleading. The generic type parameter I is contrained to be of IBaseBroker and thus ABroker can directly be assigned to BaseBroker (as a side note in that case you would not get an AV because then the same interface pointer that points to the IMT for IFooBroker would be passed down satisfying the later call to GetMsg).

However the bug is that the inherited Create call which has I as parameter (which is in this case IFooBroker) accepts the base type that it was constrained on.

 

Here is the issue a bit more condensed - and showing it does not only apply to interfaces:

 

type
  Base = class
  end;

  Child = class(Base)
  end;

  X<T: Base> = class
    procedure Test(obj: T);
  end;

  Y<T: Base> = class(X<T>)
    procedure Test(obj: T);
  end;

procedure X<T>.Test(obj: T);
begin
end;

procedure Y<T>.Test(obj: T);
var
  b: Base;
begin
  b := obj; // obj IS a Base because T is constrained to be a Base
  inherited Test(b); // this MUST NOT compile because the parameter is T and Base is not a T
end;

 

  • Thanks 2

Share this post


Link to post
10 minutes ago, Stefan Glienke said:

Here is the issue a bit more condensed - and showing it does not only apply to interfaces:

Very nice. That actually makes it very clear what the problem is  - but isn't there something missing? You've declared Child but not actually referenced it anywhere.

Maybe you meant to write:

Y<T: Child> = class(X<T>)
...

 

Share this post


Link to post
45 minutes ago, Anders Melander said:

Very nice. That actually makes it very clear what the problem is  - but isn't there something missing? You've declared Child but not actually referenced it anywhere.

Maybe you meant to write:


Y<T: Child> = class(X<T>)
...

 

While writing the code I figured that for the issue to show it does not need anything that really inherits from the type the generic is constrained on because the issue is within the generic itself. I just did not remove the Child declaration. It's just there to show that you might instantiate the generic type with anything inheriting from Base that satisfies the constraint.

 

If I had to guess I would say that when inheriting Y from X the compiler does not put the actual T into the generic type parameter but just the type it is constrained on or something - at least for the parameter checking.

Edited by Stefan Glienke

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

×