C ++ множественное виртуальное наследование против COM - PullRequest
6 голосов
/ 18 ноября 2008

Сеть переполнена объяснениями «проблемы страшного алмаза» . Так же как и StackOverflow. Я думаю, что понимаю это немного, но мне не удается перевести это знание в понимание чего-то похожего, но другого.

Мой вопрос начинается с чистого вопроса C ++, но ответ вполне может перейти в специфику MS-COM. Общий проблемный вопрос звучит так:

class Base { /* pure virtual stuff */ };
class Der1 : Base /* Non-virtual! */ { /* pure virtual stuff */ };
class Der2 : Base /* Non-virtual! */ { /* pure virtual stuff */ };
class Join : virtual Der1, virtual Der2 { /* implementation stuff */ };
class Join2 : Join { /* more implementation stuff + overides */ };

Это , а не классическое алмазное решение. Что именно здесь делает «виртуальный»?

Моя настоящая проблема - попытаться понять дискуссию у наших друзей в CodeProject. Она включает в себя собственный класс для создания прозрачного контейнера для Flash-плеера.

Я думал, что попробую это место для развлечения. Получается, что следующее объявление приводит к сбою вашего приложения с версией Flash Player версии 10.

class FlashContainerWnd:   virtual public IOleClientSite,
                           virtual public IOleInPlaceSiteWindowless,
                           virtual public IOleInPlaceFrame,
                           virtual public IStorage

Отладка показывает, что при вводе реализаций функций (QueryInterface и т. Д.) От разных вызывающих я получаю разные значения указателя «this» для разных вызовов. Но удаление «виртуального» делает свое дело! Никаких сбоев, и тот же «this» -показатель.

Я бы хотел четко понять, что именно происходит. Большое спасибо.

Приветствие Адам

Ответы [ 5 ]

3 голосов
/ 18 ноября 2008

Виртуальное наследование в первом примере ничего не делает. Держу пари, что они скомпилируются с одним и тем же кодом, если они будут удалены.

Фактически унаследованный класс просто указывает компилятору, что он должен объединить более поздние версии Der1 или Der2. Поскольку в дереве наследования появляется только один из них, ничего не делается. Виртуалы не влияют на Base.

auto p = new Join2;
static_cast<Base*>(static_cast<Der1*>(p)) !=
      static_cast<Base*>(static_cast<Der2*>(p))

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

class A {};
class B : virtual public A {};
class C : virtual public A {};
class D : public A {};
class E : virtual public A, public B, public C, public D {};
class F : public A, public B, public C, public D {};

F::A != F::B::A or F::C::A or F::D::A
F::B::A == F::C::A
F::D::A != F::B::A or F::C::A or F::A

E::B::A == E::C::A == E::A
E::D::A != E::B::A or E::C::A or E::D::A

Одна из причин, по которой A и W должны быть помечены как виртуальные в C и B вместо E или F, заключается в том, что C и B должны знать, чтобы не вызывать конструктор A. Обычно они должны были бы инициализировать каждую из своих копий. Когда они участвуют в наследовании алмазов, они не хотят. Но вы не можете перекомпилировать B и C, чтобы не создавать A. Это означает, что C и B должны знать заранее, чтобы создать код конструктора, в котором конструктор A не вызывается.

2 голосов
/ 18 ноября 2008

I думаю проблема с вашим примером COM заключается в том, что, добавляя виртуальное ключевое слово, вы говорите, что все интерфейсы IOle * имеют общую реализацию IUnknown. Чтобы реализовать это, компилятор должен создать несколько v-таблиц, поэтому вы можете использовать разные значения this в зависимости от производного класса, к которому он пришел.

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

Без виртуального наследования каждый IOle * номинально имеет собственную реализацию IUnknown. Однако, поскольку IUnknown является абстрактным классом и не имеет хранилища, компилятор и все реализации IUnknown взяты из FlashContainerWnd, поэтому существует только одна реализация.

(Хорошо, так что последний бит звучит слабо ... возможно, кто-то, кто лучше разбирается в языковых правилах, сможет объяснить это более четко)

0 голосов
/ 16 августа 2009

Как говорит Каспин, ваш первый пример на самом деле не делает ничего полезного. Однако он добавит vpointer, чтобы сообщить производным классам, где найти классы, от которых он унаследован.

Это исправляет любые алмазы, которые вы можете теперь создавать (чего у вас нет), но поскольку структура классов больше не является статической, вы больше не можете использовать static_cast для нее. Я не знаком с задействованным API, но то, что говорит Роб Уокер об IUnkown, может быть связано с этим.

Короче говоря, обычное наследование следует использовать, когда вам нужен ваш собственный базовый класс, который не должен использоваться совместно с классами 'sibling': (a - контейнер, b, c, d - части, у каждого есть контейнер, который объединяет эти части (плохой пример, почему бы не использовать композицию?))

a  a  a
|  |  |
b  c  d <-- b, c and d inherit a normally
 \ | /
   e

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

   a
 / | \
b  c  d <-- b, c and d inherit a virtually
 \ | /
   d
0 голосов
/ 10 августа 2009

Я думал, что просто попробую ваш пример. Я придумал:

#include "stdafx.h"
#include <stdio.h>

class Base
{
public:
  virtual void say_hi(const char* s)=0;
};

class Der1 : public Base
{
public:
  virtual void d1()=0;
};

class Der2 : public Base
{
public:
  virtual void d2()=0;
};

class Join : virtual public Der1, virtual public Der2
             // class Join : public Der1, public Der2
{
public:
  virtual void say_hi(const char* s);
  virtual void d1();
  virtual void d2();
};

class Join2 : public Join
{
  virtual void d1();
};

void Join::say_hi(const char* s)
{
  printf("Hi %s (%p)\n", s, this);
}

void Join::d1()
{}

void Join::d2()
{}

void Join2::d1()
{
}

int _tmain(int argc, _TCHAR* argv[])
{
  Join2* j2 = new Join2();
  Join* j = dynamic_cast<Join*>(j2);
  Der1* d1 = dynamic_cast<Der1*>(j2);
  Der2* d2 = dynamic_cast<Der2*>(j2);
  Base* b1 = dynamic_cast<Base*>(d1);
  Base* b2 = dynamic_cast<Base*>(d2);

  printf("j2: %p\n", j2);
  printf("j:  %p\n", j);
  printf("d1: %p\n", d1);
  printf("d2: %p\n", d2);
  printf("b1: %p\n", b1);
  printf("b2: %p\n", b2);

  j2->say_hi("j2");
  j->say_hi(" j");
  d1->say_hi("d1");
  d2->say_hi("d2");
  b1->say_hi("b1");
  b2->say_hi("b2");

  return 0;
}

Создает следующий вывод:

j2: 00376C10
j:  00376C10
d1: 00376C14
d2: 00376C18
b1: 00376C14
b2: 00376C18
Hi j2 (00376C10)
Hi  j (00376C10)
Hi d1 (00376C10)
Hi d2 (00376C10)
Hi b1 (00376C10)
Hi b2 (00376C10)

Таким образом, при приведении Join2 к его базовым классам вы можете получить разные указатели, но указатель this, передаваемый say_hi (), всегда один и тот же, как и ожидалось.

Так что, по сути, я не могу воспроизвести вашу проблему, затрудняя ответ на ваш настоящий вопрос.

Что касается wat "virtual", я нашел статью в wikipedia поучительно, хотя, похоже, она также фокусируется на проблеме алмазов

0 голосов
/ 19 ноября 2008

Сейчас оно немного устарело, но лучшая ссылка, которую я когда-либо встречал, касающаяся внутренних компонентов C ++, - это Липпмана Inside The C ++ Object Model. Точные детали реализации могут не соответствовать выводу вашего компилятора, но понимание, которое он предоставляет, чрезвычайно ценно.

На стр. 96 приведено объяснение виртуального наследования, в котором конкретно рассматривается проблема алмазов.

Я оставлю вас читать подробности, но в основном для использования виртуального наследования требуется поиск в виртуальной таблице, чтобы найти базовый класс. Это не относится к обычному наследованию, где местоположение базового класса может быть вычислено во время компиляции.

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

...