Почему мы не можем использовать "виртуальное наследование" в COM?

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

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

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

Ответ 1

Интерфейсы 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-in-D или B-in-something-else. Компилятору требуется некоторый ключ времени выполнения (виртуальные таблицы), чтобы найти унаследованные-из-членов. В конечном итоге вам понадобится несколько указателей на несколько виртуальных таблиц в экземпляре D, поскольку они являются vtable для унаследованного B и для унаследованного C и т.д., Что подразумевает некоторые издержки памяти.

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

ИЗМЕНИТЬ

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

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

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

ПОЛУЧИТЬ БОЛЬШЕ РЕДАКТИРОВАНИЯ

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

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

  • Для не виртуальных Dttab содержит A-in-B vtab и A-in-C vtab.
  • Для виртуальных A только один раз встречается внутри Ds vtable, но объект содержит несколько vtables, а указатели-указатели нуждаются в изменениях адреса.

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

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

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

Ответ 2

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

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

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

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

Ответ 3

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

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

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

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

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