Множественное виртуальное наследование C++ против COM

Сеть переполнена объяснениями "страшная проблема с бриллиантами". Как и 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 версии 10.

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

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

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

Ваше здоровье Адам

Я плохо разбираюсь в виртуальном наследовании. но есть ли где-нибудь в вашем приложении приведение, которое преобразует данные IOle или IStorage в FlashContainerWnd?

Johannes Schaub - litb 18.11.2008 21:50
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
6
1
2 647
5

Ответы 5

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

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

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

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

да, я думаю, ты прав. Вы получаете IUnknown, а затем используете QueryInterface, чтобы получить нужный интерфейс. Вы не создаете COM-объект обычным способом C++.

gbjbaanb 19.11.2008 01:53

Спасибо, если я правильно понял, что вы ответили правильно, я должен использовать «виртуальный», но дело в том, что я должен удалить его, чтобы остановить сбой. Поэтому я не понимаю, как это отвечает на мой вопрос.

Adam 19.11.2008 11:41

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

Практически унаследованный класс просто указывает компилятору, что он должен объединить более поздние версии 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 должен быть помечен как виртуальный в C и B вместо E или F, заключается в том, что C и B должны знать, чтобы не вызывать конструктор A. Обычно они инициализируют каждую из своих копий. Когда они участвуют в наследовании бриллиантов, они этого не делают. Но вы не можете перекомпилировать B и C, чтобы не создавать A. Это означает, что C и B должны знать заранее, чтобы создать код конструктора, в котором конструктор A не вызывается.

Я не совсем понимаю, что вы имеете в виду под «виртуальными экземплярами». Если вы имеете в виду виртуальное наследование подсетей, то моя реальная проблема, к сожалению, доказывает, что ваш ответ неверен. Потому что у меня виртуальный только на одном уровне. С виртуальным: сбой. Без виртуального: хорошо.

Adam 19.11.2008 11:54

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

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

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

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

Спасибо, но это нет проблема с бриллиантами, если вы внимательно прочитали мой вопрос.

Adam 19.11.2008 11:32

Думал просто попробовать твой пример. Я придумал:

#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 (), всегда один и тот же, в значительной степени, как и ожидалось.

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

Что касается того, что «виртуальный» делает, я нашел статью о википедия поучительной, хотя она, похоже, тоже сосредоточена на проблеме с алмазом.

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

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

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

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

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