Почему мы не можем использовать «виртуальное наследование» в COM? - PullRequest
10 голосов
/ 21 июня 2010

Я прочитал какое-то смутное утверждение, что виртуальное наследование не обеспечивает структуру памяти, необходимую для COM, поэтому мы должны использовать обычное наследование.Виртуальное наследование изобретено для решения проблемы diamond .

Может ли кто-нибудь показать мне иллюстрацию различия деталей структуры памяти между этими двумя подходами наследования?И ключевая причина , почему виртуальное наследование не подходит для COM.Картинка была бы лучшей.

Большое спасибо.

Ответы [ 3 ]

4 голосов
/ 26 июня 2010

COM-класс COM может реализовывать несколько интерфейсов, но каждый отдельный интерфейс должен реализовывать v-таблицу с указателями на все методов, введенных его «базовыми» интерфейсами. Как минимум IUnknown. Если он, скажем, реализует IPersistFile, то он должен обеспечить реализацию трех методов IUnknown, а также IPersist :: GetClassID. И специальные методы IPersistFile.

Что соответствует поведению большинства компиляторов C ++, когда они реализуют не виртуальное множественное наследование. Компилятор устанавливает отдельные v-таблицы для каждого унаследованного (чисто абстрактного) класса. И заполняет его указателями методов, так что один общий метод класса реализует все методы, которые имеют общие интерфейсы. Другими словами, независимо от того, сколько интерфейсов реализовано, все они обслуживаются одним методом класса, таким как QueryInterface, AddRef или Release.

Именно так, как вы хотели бы, чтобы это работало. Наличие одной реализации AddRef / Release делает подсчет ссылок простым, чтобы поддерживать объект coclass, независимо от того, сколько указателей интерфейса вы раздаете. QueryInterface легко реализовать, простое приведение предоставляет указатель интерфейса на v-таблицу с правильной компоновкой.

Виртуальное наследование не требуется. И, скорее всего, сломает COM, потому что v-таблицы больше не имеют требуемой разметки. Что сложно для любого компилятора, рассмотрите параметры / vm для компилятора MSVC, например. То, что COM настолько странно совместим с типичным поведением компилятора C ++, не случайность.

Кстати, все это поражает поклонника, когда коклассу нужно реализовать несколько интерфейсов, имеющих общее имя метода, которое не предназначено для того же. Это довольно большой упс, и с ним трудно иметь дело. Упомянутый в ATL Internals (DAdvise?), Я к сожалению забыл решение.

4 голосов
/ 21 июня 2010

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

Но вы правы, что это не тот же механизм, что и виртуальныйнаследование в C ++.C ++ не использует функцию QueryInterface для выгрузки, поэтому ему нужен другой способ получения указателя базового класса.

Проблема размещения памяти вызвана тем, что COM требует, чтобы все методы базового интерфейса могли быть вызванынепосредственно используя производный указатель интерфейса.AddRef хороший пример.В COM вы можете вызвать AddRef и передать любой производный интерфейс как указатель this.В C ++ реализация AddRef ожидает, что указатель this будет иметь тип IUnknown* const.Разница в том, что в C ++ вызывающий находит базовый указатель, а в COM вызываемый выполняет настройку для поиска базового указателя, поэтому каждому производному интерфейсу нужна отдельная реализация (по крайней мере QueryInterface), учитывающая смещение отуказатель производного интерфейса передается в базовый указатель.

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

2 голосов
/ 30 июня 2010

COM-интерфейсы в некотором роде похожи на JAVA-интерфейсы - в них нет элементов данных. Это означает, что при использовании множественного наследования наследование интерфейса отличается от наследования классов.

Для начала рассмотрим не виртуальное наследование с ромбовидными шаблонами наследования ...

  • B наследует A
  • C наследует A
  • D наследует B и C

Экземпляр D содержит два отдельных экземпляра членов данных A. Это означает, что когда указатель на A указывает на экземпляр D, он должен определить, какой экземпляр A в D это означает - указатель отличается в каждом случае, и приведение указателей не является простым повторным обозначением типа - адрес тоже меняется.

Теперь рассмотрим тот же бриллиант с виртуальным наследованием. Все экземпляры B, C и D содержат один экземпляр A. Если вы думаете, что B и C имеют фиксированный макет (включая экземпляр A), это проблема. Если макет Bs [A, x] и макет Cs [A, y], то [B, C, z] недопустим для D - он будет содержать два экземпляра A. То, что вам нужно использовать, это что-то вроде [ A, B ', C', z], где B '- все от B, кроме унаследованного A и т. Д.

Это означает, что если у вас есть указатель на B, у вас нет единой схемы для разыменования членов, унаследованных от A. Поиск этих членов отличается в зависимости от того, указывает ли указатель на чистый B или B-внутри-D или B-внутри-что-то еще. Компилятору требуется некоторая подсказка времени выполнения (виртуальные таблицы), чтобы найти унаследованные члены от A. В конечном итоге вам понадобится несколько указателей на несколько виртуальных таблиц в экземпляре D, поскольку существует vtable для унаследованного B, унаследованного C и т. Д., Что подразумевает некоторые накладные расходы памяти.

Одиночное наследование не имеет этих проблем. Структура памяти экземпляров остается простой, а виртуальные таблицы также проще. Вот почему Java запрещает множественное наследование для классов. В наследовании интерфейса нет элементов данных, поэтому опять эти проблемы просто не возникают - нет проблемы, которая-унаследована-с-D, и нет разных способов найти A-в-B, в зависимости от того, что этот конкретный B бывает внутри. И COM, и Java могут разрешать множественное наследование интерфейсов, не справляясь с этими сложностями.

EDIT

Я забыл сказать - без элементов данных нет реальной разницы между виртуальным и не виртуальным наследованием. Однако в Visual C ++ компоновка, вероятно, отличается, даже если нет элементов данных - используются одинаковые правила для каждого стиля наследования последовательно, независимо от того, присутствуют ли какие-либо элементы данных.

Кроме того, макет памяти COM соответствует макету Visual-C ++ (для поддерживаемых типов наследования), поскольку он предназначен для этого. Нет причины, по которой COM не мог бы быть разработан для поддержки множественного и виртуального наследования «интерфейсов» с элементами данных. Microsoft могла бы разработать COM для поддержки той же модели наследования, что и C ++, но предпочла этого не делать - и нет никаких причин, почему они должны были поступить иначе.

Ранний COM-код часто был написан на C, что означало рукописные структурные макеты, которые должны были точно соответствовать макету Visual-C ++ для работы. Макеты для множественного и виртуального наследования - ну, я бы не стал делать это вручную. Кроме того, COM всегда был своим собственным средством, предназначенным для связи кода, написанного на разных языках. Он никогда не был предназначен для привязки к C ++.

ЕЩЕ БОЛЬШЕ РЕДАКТИРОВАНИЯ

Я понял, что упустил ключевой момент.

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

  • Для не-виртуальных D-VAB содержит V-A-V-B и V-C-A-V.
  • Для виртуального, A встречается только один раз в Ds vtable, но объект содержит несколько vtables, и приведение указателей требует изменения адреса.

С наследованием интерфейса, это в основном детали реализации - есть только один набор реализаций метода для A.

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...