Why exactly do I need an explicit upcast when implementing QueryInterface() in an object with multiple interfaces()
The problem is that *ppv
is usually a void*
- directly assigning this
to it will simply take the existing this
pointer and give *ppv
the value of it (since all pointers can be cast to void*
).
This is not a problem with single inheritance because with single inheritance the base pointer is always the same for all classes (because the vtable is just extended for the derived classes).
However - for multiple inheritance you actually end up with multiple base pointers, depending on which 'view' of the class you're talking about! The reason for this is that with multiple inheritance you can't just extend the vtable - you need multiple vtables depending on which branch you're talking about.
So you need to cast the this
pointer to make sure that the compiler puts the correct base pointer (for the correct vtable) into *ppv
.
Here's an example of single inheritance:
class A {
virtual void fa0();
virtual void fa1();
int a0;
};
class B : public A {
virtual void fb0();
virtual void fb1();
int b0;
};
vtable for A:
[0] fa0
[1] fa1
vtable for B:
[0] fa0
[1] fa1
[2] fb0
[3] fb1
Note that if you have the B
vtable and you treat it like an A
vtable it just works - the offsets for the members of A
are exactly what you would expect.
Here's an example using multiple inheritance (using definitions of A
and B
from above) (note: just an example - implementations may vary):
class C {
virtual void fc0();
virtual void fc1();
int c0;
};
class D : public B, public C {
virtual void fd0();
virtual void fd1();
int d0;
};
vtable for C:
[0] fc0
[1] fc1
vtable for D:
@A:
[0] fa0
[1] fa1
[2] fb0
[3] fb1
[4] fd0
[5] fd1
@C:
[0] fc0
[1] fc1
[2] fd0
[3] fd1
And the actual memory layout for D
:
[0] @A vtable
[1] a0
[2] b0
[3] @C vtable
[4] c0
[5] d0
Note that if you treat a D
vtable as an A
it will work (this is coincidence - you can't rely on it). However - if you treat a D
vtable as a C
when you call c0
(which the compiler expects in slot 0 of the vtable) you'll suddenly be calling a0
!
When you call c0
on a D
what the compiler does is it actually passes a fake this
pointer which has a vtable which looks the way it should for a C
.
So when you call a C
function on D
it needs to adjust the vtable to point to the middle of the D
object (at the @C
vtable) before calling the function.
You're doing COM programming, so there are a few things to recall about your code before looking at why QueryInterface
is implemented the way it is.
- Both
IInterface1
andIInterface2
descend fromIUnknown
, and let's assume neither is a descendant of the other. - When something calls
QueryInterface(IID_IUnknown, (void**)&intf)
on your object,intf
will be declared as typeIUnknown*
. - There are multiple "views" of your object — interface pointers — and
QueryInterface
could be called through any one of them.
Because point #3, the value of this
in your QueryInterface
definition can vary. Call the function via an IInterface1
pointer, and this
will have a different value than it would if it were called via an IInterface2
pointer. In either case, this
will hold a valid pointer of type IUnknown*
because of point #1, so if you simply assign *ppv = this
, the caller will be happy, from a C++ point of view. You'll have stored a value of type IUnknown*
into a variable of that same type (see point #2), so everything's fine.
However, COM has stronger rules than ordinary C++. In particular, it requires that any request for the IUnknown
interface of an object must return the same pointer, no matter which "view" of that object was used to invoke the query. Therefore, it's not sufficient for your object to always assign mere this
into *ppv
. Sometimes callers would get the IInterface1
version, and sometimes they'd get the IInterface2
version. A proper COM implementation needs to make sure it returns consistent results. It will commonly have an if
-else
ladder checking for all supported interfaces, but one of the conditions will check for two interfaces instead of just one, the second being IUnknown
:
if (iid == IID_IUnknown || iid == IID_IInterface1) {
*ppv = static_cast<IInterface1*>(this);
} else if (iid == IID_IInterface2) {
*ppv = static_cast<IInterface2*>(this);
} else {
*ppv = NULL;
return E_NOINTERFACE;
}
AddRef();
return S_OK;
It doesn't matter which interface the IUnknown
check is grouped with as long as the grouping doesn't change while the object still exists, but you'd really have to go out of your way to make that happen.