Почему именно мне нужен явный прирост при реализации QueryInterface () в объекте с несколькими интерфейсами () - PullRequest
14 голосов
/ 16 ноября 2009

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

Ответы [ 2 ]

26 голосов
/ 16 ноября 2009

Проблема в том, что *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 vtable), вы внезапно будете вызывать a0!

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

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

7 голосов
/ 11 мая 2010

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

...