Подтвердить что ты не робот

Почему именно мне нужен явный upcast при реализации QueryInterface() в объекте с несколькими интерфейсами()

Предположим, что у меня есть класс, реализующий два или более COM-интерфейса:

class CMyClass : public IInterface1, public IInterface2 {
};

Почти каждый документ, который я видел, показывает, что когда я реализую QueryInterface() для IUnknown, я явно повышаю этот указатель на один из интерфейсов:

if( iid == __uuidof( IUnknown ) ) {
     *ppv = static_cast<IInterface1>( this );
     //call Addref(), return S_OK
}

Вопрос в том, почему я не могу просто скопировать это?

if( iid == __uuidof( IUnknown ) ) {
     *ppv = this;
     //call Addref(), return S_OK
}

В документах обычно говорится, что если я сделаю последнее, я нарушу требование о том, что любой вызов QueryInterface() на том же объекте должен возвращать точно такое же значение.

Я не совсем понимаю. Они означают, что если я QI() для IInterface2 и вызовет QueryInterface() через этот указатель С++, это будет немного отличаться от if я QI() для IInterface2, потому что С++ будет каждый раз делать эту точку в подобъекте?

4b9b3361

Ответ 1

Проблема заключается в том, что *ppv обычно является void* - непосредственно назначая this, чтобы он просто взял существующий указатель this и дал *ppv значение его (так как все указатели могут быть отброшены void*).

Это не проблема с одиночным наследованием, потому что при единственном наследовании базовый указатель всегда одинаковый для всех классов (поскольку vtable просто расширяется для производных классов).

Однако - для множественного наследования вы фактически получаете несколько базовых указателей, в зависимости от того, какой "вид" класса, о котором вы говорите! Причиной этого является то, что при множественном наследовании вы не можете просто расширить vtable - вам нужно несколько vtables в зависимости от того, к какой ветки вы говорите.

Итак, вам нужно указать указатель this, чтобы убедиться, что компилятор помещает правильный указатель базы (для правильной таблицы vtable) в *ppv.

Здесь пример одиночного наследования:

class A {
  virtual void fa0();
  virtual void fa1();
  int a0;
};

class B : public A {
  virtual void fb0();
  virtual void fb1();
  int b0;
};

vtable для A:

[0] fa0
[1] fa1

vtable для B:

[0] fa0
[1] fa1
[2] fb0
[3] fb1

Обратите внимание, что если у вас есть B vtable, и вы относитесь к нему как к A vtable, это просто работает - смещения для членов A - это именно то, что вы ожидаете.

Здесь пример использования множественного наследования (с использованием определений A и B сверху) (примечание: только пример - реализация может различаться):

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 для C:

[0] fc0
[1] fc1

vtable для D:

@A:
[0] fa0
[1] fa1
[2] fb0
[3] fb1
[4] fd0
[5] fd1

@C:
[0] fc0
[1] fc1
[2] fd0
[3] fd1

И фактическая схема памяти для D:

[0] @A vtable
[1] a0
[2] b0
[3] @C vtable
[4] c0
[5] d0

Обратите внимание, что если вы рассматриваете D vtable как A, он будет работать (это совпадение - вы не можете положиться на него). Однако, если вы обрабатываете vtable D как C, когда вы вызываете c0 (который компилятор ожидает в слоте 0 таблицы vtable), вы неожиданно вызываете a0!

Когда вы вызываете c0 на D, что делает компилятор, он фактически передает поддельный указатель this, который имеет vtable, который выглядит так, как должен для C.

Поэтому, когда вы вызываете функцию C на D, перед вызовом функции необходимо настроить vtable, чтобы указать на середину объекта D (на @C vtable).

Ответ 2

Вы программируете COM, поэтому есть несколько вещей, которые нужно вспомнить о вашем коде, прежде чем смотреть, почему QueryInterface реализуется так, как есть.

  • Оба IInterface1 и IInterface2 сходят с IUnknown, и пусть не предполагается, что ни один из потомков другого.
  • Когда что-то вызывает QueryInterface(IID_IUnknown, (void**)&intf) на вашем объекте, intf будет объявлен как тип IUnknown*.
  • Существует несколько "представлений" вашего объекта - указатели интерфейса - и QueryInterface можно вызвать через любой из них.

Поскольку точка №3, значение this в вашем определении QueryInterface может отличаться. Вызвать функцию с помощью указателя IInterface1, а this будет иметь другое значение, чем если бы оно было вызвано с помощью указателя IInterface2. В любом случае this будет содержать допустимый указатель типа IUnknown* из-за точки # 1, поэтому, если вы просто назначаете *ppv = this, вызывающий объект будет счастлив с точки зрения С++. Вы сохраните значение типа IUnknown* в переменной того же типа (см. Пункт 2), так что все отлично.

Однако COM имеет более сильные правила, чем обычные С++. В частности, это требует, чтобы любой запрос для интерфейса IUnknown объекта должен возвращать тот же указатель, независимо от того, какой "вид" этого объекта использовался для вызова запроса. Поэтому недостаточно для вашего объекта всегда присваивать простой this в *ppv. Иногда вызывающие пользователи получают версию IInterface1, и иногда они получают версию IInterface2. Для правильной реализации COM необходимо убедиться, что он возвращает согласованные результаты. Как правило, для всех поддерживаемых интерфейсов проверка лестницы if - else выполняется, но одно из условий будет проверять наличие двух интерфейсов вместо одного, второе - 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;

Не имеет значения, какой интерфейс проверяет флажок IUnknown, пока группировка не изменяется, пока объект все еще существует, но вам действительно нужно будет сбиться с пути, чтобы это произошло.