In Delphi, how can you check if an IInterface reference implements a derived but not explicitly-supported interface?

delphi, delphi-2007, interface, types, winapi

Solution

One thing you can do is stop type-casting interfaces. You don't need to do it to go from `IDerived` to `IBase`, and you don't need it to go from `IBase` to `IUnknown`, either. Any reference to an `IDerived` is an `IBase` already, so you can call `IBase` methods even without type-casting. If you do less type-casting, you let the compiler do more work for you and catch things that aren't sound.

Your stated goal is to be able to check that the thing you're getting out of your list really is an `IBase` reference. Adding `IBase` as an implemented interface would allow you to achieve that goal easily. In that light, your "two major reasons" for not doing that don't hold any water.

"I want to be able to compare references for equality": No problem. COM requires that if you call `QueryInterface` twice with the same GUID on the same object, you get the same interface pointer both times. If you have two arbitrary interface references, and you `as`-cast them both to `IBase`, then the results will have the same pointer value if and only if they are backed by the same object.

Since you seem to want your list to only contain `IBase` values, and you don't have Delphi 2009 where a generic `TInterfaceList<IBase>` would be helpful, you can discipline yourself to always explicitly add `IBase` values to the list, never values of any descendant type. Whenever you add an item to the list, use code like this:

List.Add(Item as IBase);

That way, any duplicates in the list are easy to detect, and your "hard casts" are assured to work.

"It doesn't actually solve the problem": But it does, given the rule above.

Assert(Supports(List[i], IBase));

When the object explicitly implements all its interfaces, you can check for things like that. And if you've added items to the list like I described above, it's safe to disable the assertion. Enabling the assertion lets you detect when someone has changed code elsewhere in your program to add an item to the list incorrectly. Running your unit tests frequently will let you detect the problem very soon after it's introduced, too.

With the above points in mind, you can check that anything that was added to the list was added correctly with this code:

var
  AssertionItem: IBase;

Assert(Supports(List[i], IBase, AssertionItem)
       and (AssertionItem = List[i]));
// I don't recall whether the compiler accepts comparing an IBase
// value (AssertionItem) to an IUnknown value (List[i]). If the
// compiler complains, then simply change the declaration to
// IUnknown instead; the Supports function won't notice.

If the assertion fails, then either you added something to the list that doesn't support `IBase` at all, or the specific interface reference you added for some object cannot serve as the `IBase` reference. If the assertion passes, then you know that `List[i]` will give you a valid `IBase` value.

Note that the value added to the list doesn't need to be an `IBase` value explicitly. Given your type declarations above, this is safe:

var
  A: IDerived;
begin
  A := TImplementation.Create;
  List.Add(A);
end;

That's safe because the interfaces implemented by `TImplementation` form an inheritance tree that degenerates to a simple list. There are no branches where two interfaces don't inherit from each other but have a common ancestor. If there were two decendants of `IBase`, and `TImplementation` implemented them both, the above code wouldn't be valid because the `IBase` reference held in `A` wouldn't necessarily be the "canonical" `IBase` reference for that object. The assertion would detect that problem, and you'd need to add it with `List.Add(A as IBase)` instead.

When you disable assertions, the cost of getting the types right is paid only while adding to the list, not while reading from the list. I named the variable `AssertionItem` to discourage you from using that variable elsewhere in the procedure; it's there only to support the assertion, and it won't have a valid value once assertions are disabled.

Problem

If I have the following interfaces and a class that implements them - ``` IBase = Interface ['{82F1F81A-A408-448B-A194-DCED9A7E4FF7}'] End; IDerived = Interface(IBase) ['{A0313EBE-C50D-4857-B324-8C0670C8252A}'] End; TImplementation = Class(TInterfacedObject, IDerived) End; ``` The following code prints 'Bad!' - ``` Procedure Test; Var A : IDerived; Begin A := TImplementation.Create As IDerived; If Supports (A, IBase) Then WriteLn ('Good!') Else WriteLn ('Bad!'); End; ``` This is a little annoying but understandable. Supports can't cast to IBase because IBase is not in the list of GUIDs that TImplementation supports. It can be fixed by changing the declaration to - ``` TImplementation = Class(TInterfacedObject, IDerived, IBase) ``` Yet even without doing that I already know that A implements IBase because A is an IDerived and an IDerived is an IBase. So if I leave out the check I can cast A and everything will be fine - ``` Procedure Test; Var A : IDerived; B : IBase; Begin A := TImplementation.Create As IDerived; B := IBase(A); //Can now successfully call any of B's methods End; ``` But we come across a problem when we start putting IBases into a generic container - TInterfaceList for example. It can only hold IInterfaces so we have to do some casting. ``` Procedure Test2; Var A : IDerived; B : IBase; List : TInterfaceList; Begin A := TImplementation.Create As IDerived; B := IBase(A); List := TInterfaceList.Create; List.Add(IInterface(B)); Assert (Supports (List[0], IBase)); //This assertion fails IBase(List[0]).DoWhatever; //Assuming I declared DoWhatever in IBase, this works fine, but it is not type-safe List.Free; End; ``` I would very much like to have some sort of assertion to catch any mismatched types - this sort of thing can be done with objects using the Is operator, but that doesn't work for interfaces. For various reasons, I don't want to explicitly add IBase to the list of supported interfaces. Is there any way I can write TImplementation and the assertion in such a way that it will evaluate to true iff hard-casting IBase(List[0]) is a safe thing to do? Edit: As it came up in the one of the answers, I'm adding the two major reasons I do not want to add IBase to the list of interfaces that TImplementation implements. Firstly, it doesn't actually solve the problem. If, in Test2, the expression: ``` Supports (List[0], IBase) ``` returns true, this does not mean it is safe to perform a hard-cast. QueryInterface may return a different pointer to satisfy the requested interface. For example, if TImplementation explicitly implements both IBase and IDerived (and IInterface), then the assertion will pass successfully: ``` Assert (Supports (List[0], IBase)); //Passes, List[0] does implement IBase ``` But imagine that somebody mistakenly adds an item to the list as an IInterface ``` List.Add(Item As IInterface); ``` The assertion still passes - the item still implements IBase, but the reference added to the list is an IInterface only - hard-casting it to an IBase would not produce anything sensible, so the assertion isn't sufficient in checking whether the following hard-cast is safe. The only way that's guaranteed to work would be to use an as-cast or supports: ``` (List[0] As IBase).DoWhatever; ``` But this is a frustrating performance cost, as it is intended to be the responsibility of the code adding items to the list to ensure they are of the type IBase - we should be able to assume this (hence the assertion to catch if this assumption is false). The assertion isn't even necessary, except to catch later mistakes if anyone changes some of the types. The original code this problem comes from is also fairly performance critical, so a performance cost that achieves little (it still only catches mismatched types at run-time, but without the possibility to compile a faster release build) is something I'd rather avoid. The second reason is I want to be able to compare references for equality, but this can't be done if the same implementation object is held by different references with different VMT offsets. Edit 2: Expanded the above edit with an example. Edit 3: Note: The question is how can I formulate the assertion so that the hard-cast is safe iff the assertion passes, not how to avoid the hard-cast. There are ways to do the hard-cast step differently, or to avoid it completely, but if there is a runtime performance cost, I can't use them. I want all the cost of checking within the assertion so that it can be compiled out later. Having said that, if someone can avoid the problem altogether with no performance cost and no type-checking danger that would be great!

Original source