Почему именно мне нужен явный прирост при реализации 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() через этот указатель C++, то это будет немного отличаться от того, если я QI() для IInterface2, потому что C++ будет каждый раз указывать на подобъект?

2 ответа

Решение

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

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

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

Так что вам нужно разыграть this указатель, чтобы убедиться, что компилятор помещает правильный базовый указатель (для правильной таблицы) в *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 это сработает (это совпадение - на это нельзя полагаться). Однако - если вы относитесь к D Vtable как C когда ты звонишь c0 (что компилятор ожидает в слоте 0 виртуальной таблицы) вы вдруг будете вызывать a0!

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

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

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

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

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

Тем не менее, COM имеет более строгие правила, чем обычный C++. В частности, требуется, чтобы любой запрос 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 check сгруппирован до тех пор, пока группировка не изменится, пока объект все еще существует, но вам действительно придется приложить все усилия, чтобы это произошло.

Другие вопросы по тегам