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.

Leave a Comment