Почему именно мне нужен явный прирост при реализации 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
реализуется так, как оно есть.
- И то и другое
IInterface1
а такжеIInterface2
спуститься сIUnknown
, и давайте предположим, что ни один не является потомком другого. - Когда что-то звонит
QueryInterface(IID_IUnknown, (void**)&intf)
на вашем объекте,intf
будет объявлен как типIUnknown*
, - Есть несколько "просмотров" вашего объекта - указатели интерфейса - и
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 сгруппирован до тех пор, пока группировка не изменится, пока объект все еще существует, но вам действительно придется приложить все усилия, чтобы это произошло.