It is always okay to upcast a COM interface pointer
The actual spec can be found here, in MS-Word format (Microsoft no longer hosts it to my understanding):
http://web.archive.org/web/20030201093710/http://www.microsoft.com/Com/resources/comdocs.asp
It is very easy to read, and clarifies a lot of details about COM which aren't made obvious on MS-docs currently.
Per the spec, a COM interface pointer is defined as a pointer to a pointer to an array of function pointers (called the VTable). The first argument to every entry in the VTable is the interface pointer itself. An interface "Inherits" from another interface by listing the other interface's functions first, followed by its own. Note, that this requirement means that only single-inheritance is supported, as explained in The COM Specification, chapter 2 part 1.2 (emphasis mine):
Interfaces and Inheritance
COM separates class hierarchy (or indeed any other implementation technology) from interface hierarchy and both of those from any implementation hierarchy. Therefore, interface inheritance is only applied to reuse the definition of the contract associated with the base interface. There is no selective inheritance in COM: if one interface inherits from another, it includes all the functions that the other interface defines, for the same reason than an object must implement all interface functions it inherits.
Inheritance is used sparingly in the COM interfaces. Most of the pre-defined interfaces inherit directly from IUnknown (to receive the fundamental functions like QueryInterface), rather than inheriting from another interface to add more functionality. Because COM interfaces are inherited from IUnknown, they tend to be small and distinct from one another. This keeps functionality in separate groups that can be independently updated from the other interfaces, and can be recombined with other interfaces in semantically useful ways.
In addition, interfaces only use single inheritance, never multiple inheritance, to obtain functions from a base interface. Providing otherwise would significantly complicate the interface method call sequence, which is just an indirect function call, and, further, the utility of multiple inheritance is subsumed within the capabilities provided by QueryInterface.
Note that last portion: The COM design uses single-inheritance specifically so that it is simple to call the base interface from a child interface, i.e. specifically so that you can just cast the interface pointer.
The structure of the interface VTable (and therefore, the viability of pointer casting) is confirmed in several other places as well. See the description of inheritance in the description of the IDL, chapter 13, part 1.4 (emphasis mine):
Interface Inheritance
Single inheritance of interfaces is supported, using the C++ notation for same. Referring again to [CAE RPC], page 238:
<interface_header> ::=
<[> <interface_attributes> <]> interface <Identifier> [ <:> <Identifier> ]
For example:
[object, uuid(b5483f00-4f6c-101b-a1c7-00aa00389acb)]
interface IBar : IWazoo {
HRESULT Bar([in] short i, [in] IFoo * pIF);
};
cases the first methods in IBar to be the methods of IWazoo.
Pointer casting is even explicitly called out as okay when describing inheritance from IUknown in Chapter 3, part 1.3 (emphasis mine):
The IUnknown Interface
This specification has already mentioned the IUnknown interface many times. It is the fundamental interface in COM that contains basic operations of not only all objects, but all interfaces as well: reference counting and
QueryInterface. All interfaces in COM are polymorphic with IUnknown, that is, if you look at the first three functions in any interface you see QueryInterface, AddRef, and Release. In other words, IUnknown is base interface from which all other interfaces inherit.
Any single object usually only requires a single implementation of the IUnknown member functions. This means that by virtue of implementing any interface on an object you completely implement the IUnknown functions. You do not generally need to explicitly inherit from nor implement IUnknown as its own interface: when queried for it, simply typecast another interface pointer into an IUnknown* which is entirely legal with polymorphism.
We can also confirm the interface layout by examining the header file definitions for those interfaces, which all include a vTable which contains the base vTable as a first element (to use d2d1.h as an example):
typedef struct ID2D1ResourceVtbl {
IUnknownVtbl Base;
STDMETHOD_(void, GetFactory)(ID2D1Resource *This, ID2D1Factory **factory) PURE;
} ID2D1ResourceVtbl;
In fact, the very functions one might use to avoid casting when calling base interface methods are actually macros which perform casts!
#define ID2D1Resource_QueryInterface(this,A,B) (this)->lpVtbl->Base.QueryInterface((IUnknown*)(this),A,B)
^^^^^^^^^
Pointer upcasts don't just work by coincidence, it's both guaranteed by the spec, and verifiable in your header files.
Addressing concerns raised by others:
What about pointer adjustments?
Because COM only supports single, pure-virtual inheritance, there is never a need for pointer adjustments when performing a cast. This is deliberate, as pointer adjustments when casting are very C++-specific behavior and COM tries to be language-independent. You can confirm this by looking at the machine-code output by your C++ compiler when casting COM interface pointers. There will be no adjustments.
What about different interface method implementations based on which interface the pointer actually points to?
That's why you always call functions using the vTable! This is the magic of polymorphism: that a function can be dispatched dynamically based on the type of the object without the caller knowing the type of the object. An object theoretically could do something differently based on which interface pointer you used to call a given interface method, but that's its business, not the client's. (edit: I've added some more details about both this and reference counting below)
But what about separate reference counts for different interfaces?
Again, that's why you always call functions using the vTable. The call is guaranteed to be dispatched to the correct place. There would be no point in having a vTable otherwise (we could just use static dispatch). Note that you should call AddRef whenever you make a copy of an interface pointer, regardless of whether or not that copy was made using a cast.
Final Notes/Warnings:
- Just because you can always cast to a base interface does not mean that the pointer you get is the same one that would have been returned had you called QueryInterface. This detail isn't important most of the time but comes into play if you ever have check whether two interfaces belong to the same COM object. You can't just compare the interface pointers directly; you have to compare the values returned by calling QueryInterface(IID_IUnknown, ...) on both.
- "Up"-casting (implicit casting) is also perfectly valid to do in C++, since C++ compilers are required to conform to similar ABI requirements to what COM lays out (by no coincidence).
- Pointer-casting is not okay for "cross"-casting or "down"-casting, only "up"-casting (an implicit cast in C++). If I1 does not inherit from I2, you cannot cast to an I2.
- The first argument to an interface method should only ever be the interface pointer from which you got lpVtbl. Such a mistake should be pretty easy to spot in C, and is not even possible in C++.
- Notably, the current documentation on COM as hosted on MS-docs is edited slightly from the original spec to remove alot of the concrete details and real-life examples. Specifically, it is difficult to find examples/explanations which involve C, which might mislead the reader into believing that the rules surrounding inheritance simply follow C++ inheritance rules, rather than a very strict subset of them.
In Short:
The example you gave with typecasting will work, because ID3D12GraphicsCommandList inherits from ID3D12CommandList.
Edit: More Notes on interface vs implementation:
As stated above, it is perfectly legal for a COM object to return different pointer from QueryInterface() to the one you would get by upcasting. Some have pointed out that this opens the door for the pointers in the interface vTable to point to completely different functions, and therefore result in different object behaviors when called. While it would be incredibly confusing and awful for a COM object to do this in a way that's visible to the API consumer, it is nevertheless legal per the COM spec. This does not mean that "always use QueryInterface() and never upcast the pointer" is good advice, though.
Note: To reduce confusion, I'm going to call the behavior that you get from the child interface (same as the behavior you get when casting a pointer) the "inherited" behavior, while the behavior of the interface returned by QueryInterface will be the "queried" behavior.
Firstly: different "inherited" and "queried" behaviors would be such a strange thing to do, that any reasonable designer would have to put that information in the object's documentation. It doesn't make sense for the API consumer to always try to account for the possibility of such weirdness, in the same way that it wouldn't make sense to only call COM methods on a Tuesday just because it's not illegal per the spec for a COM object to change its behavior based on the day of the week.
Secondly: There's no guarantee that the "queried" behavior is what you want vs the "inherited" behavior. In fact, if our theoretical object were deliberately designed to have different per-interface behaviors, you would probably want the "inherited" behavior, since it might be more likely to play nice with whatever you were doing with the child interface to begin with.
Finally: Reference counting will continue to work just fine, even if different interfaces correspond to different reference counts. I discussed this somewhat above, but I'll be more specific here. You should always call Release on the same pointer you used to call AddRef, regardless of its type (same type != same pointer, even if they are from the same object). Because the calls to AddRef and Release are dispatched dynamically (the whole point of having a vTable!), calling Release on the correct pointer will decrement the correct reference count. Avoiding pointer upcasting will not save you if you handle this incorrectly.
As an aside: your code should not use the exact value of the reference count for any reason. COM exposes this value for debugging purposes only (say, for when you're tracking down a memory leak), and it is not intended for general program use. Client code's only concern is calling "AddRef" and "Release" at the right times, and the server is responsible for the rest.
Here is some info on MS-Docs about reference counts which might be helpful:
https://learn.microsoft.com/en-us/windows/win32/learnwin32/managing-the-lifetime-of-an-object