Я хочу знать, что такое " виртуальный базовый класс" » и что это значит.
Позвольте мне показать пример:
class Foo
{
public:
void DoSomething() { /* ... */ }
};
class Bar : public virtual Foo
{
public:
void DoSpecific() { /* ... */ }
};
c++
virtual-inheritance
popopome
источник
источник
Ответы:
Виртуальные базовые классы, используемые в виртуальном наследовании, являются способом предотвращения появления нескольких «экземпляров» данного класса в иерархии наследования при использовании множественного наследования.
Рассмотрим следующий сценарий:
Приведенная выше иерархия классов приводит к «страшному алмазу», который выглядит следующим образом:
Экземпляр D будет состоять из B, который включает в себя A, и C, который также включает в себя A. Таким образом, у вас есть два «экземпляра» (для лучшего выражения) из A.
Когда у вас есть этот сценарий, у вас есть возможность двусмысленности. Что происходит, когда вы делаете это:
Виртуальное наследование призвано решить эту проблему. Когда вы указываете virtual при наследовании ваших классов, вы сообщаете компилятору, что вам нужен только один экземпляр.
Это означает, что в иерархию включен только один «экземпляр» A. следовательно
Это мини-резюме. Для получения дополнительной информации, прочитайте это и это . Хороший пример также доступен здесь .
источник
virtual
, то расположение объекта выглядит как ромб; и если мы не используем,virtual
то расположение объекта выглядит как древовидная структура, которая содержит дваA
sО расположении памяти
В качестве примечания, проблема с Dreaded Diamond заключается в том, что базовый класс присутствует несколько раз. Таким образом, с регулярным наследованием, вы полагаете, что имеете:
Но в макете памяти у вас есть:
Это объясняет, почему при звонке у
D::foo()
вас возникает проблема неоднозначности. Но настоящая проблема возникает, когда вы хотите использовать переменную-членA
. Например, скажем, у нас есть:Когда вы будете пытаться получить доступ
m_iValue
изD
компилятор будет протестовать, потому что в иерархии, то увидит дваm_iValue
, а не один. И если вы измените один, скажем,B::m_iValue
(этоA::m_iValue
родительB
),C::m_iValue
не будет изменен (этоA::m_iValue
родительC
).Вот где виртуальное наследование становится полезным, так как с его помощью вы вернетесь к истинному алмазному макету, а не только одному
foo()
метод, но также один и только одинm_iValue
.Что может пойти не так?
Представить:
A
имеет некоторые основные функции.B
добавляет к этому какой-то классный массив данных (например)C
добавляет к этому некоторые интересные функции, такие как шаблон наблюдателя (например, наm_iValue
).D
наследует отB
иC
, следовательно, отA
.С нормальным наследованием, изменяя
m_iValue
изD
является неоднозначным, и это должно быть решено. Даже если это так, есть дваm_iValues
внутриD
, так что лучше помнить , что и обновлять два одновременно.С виртуальным наследованием, изменение
m_iValue
сD
в порядке ... Но ... Допустим, у вас естьD
. Через егоC
интерфейс вы прикрепили наблюдателя. И через егоB
интерфейс вы обновляете классный массив, побочный эффект которого заключается в прямом измененииm_iValue
...Поскольку изменение
m_iValue
выполняется напрямую (без использования метода виртуального метода доступа), «прослушивание» наблюдателяC
вызываться не будет, поскольку код, реализующий прослушивание, находится вC
иB
не знает об этом ...Вывод
Если у вас есть ромб в вашей иерархии, это означает, что у вас есть 95% вероятность, что что-то не так с этой иерархией.
источник
Для объяснения множественного наследования с помощью виртуальных баз требуется знание объектной модели C ++. И объяснение темы лучше всего делать в статье, а не в поле для комментариев.
Наилучшим понятным объяснением, которое я нашел и которое разрешило все мои сомнения по этому вопросу, была эта статья: http://www.phpcompiler.org/articles/virtualinheritance.html.
Вам действительно не нужно будет читать что-либо еще по этой теме (если вы не писатель компилятора) после прочтения этого ...
источник
Я думаю, что вы путаете две совершенно разные вещи. Виртуальное наследование - это не то же самое, что абстрактный класс. Виртуальное наследование изменяет поведение вызовов функций; иногда он разрешает вызовы функций, которые в противном случае были бы неоднозначными, иногда он переносит обработку вызовов функций на класс, отличный от того, который можно ожидать в невиртуальном наследовании.
источник
Я хотел бы добавить к добрым разъяснениям OJ.
Виртуальное наследство не обходится без цены. Как и со всеми виртуальными вещами, вы получаете удар по производительности. Этот хит производительности может быть менее элегантным.
Вместо того, чтобы разбивать алмаз путем виртуального извлечения, вы можете добавить еще один слой к алмазу, чтобы получить что-то вроде этого:
Ни один из классов не наследует виртуально, все наследуют публично. Классы D21 и D22 будут затем скрывать виртуальную функцию f (), которая неоднозначна для DD, возможно, объявив функцию частной. Каждый из них определил бы функцию-оболочку, f1 () и f2 () соответственно, каждый из которых вызывал бы локальный класс (private) f (), таким образом разрешая конфликты. Класс DD вызывает f1 (), если он хочет D11 :: f () и f2 (), если он хочет D12 :: f (). Если вы определите встроенные оболочки, вы, вероятно, получите около нуля накладных расходов.
Конечно, если вы можете изменить D11 и D12, вы можете сделать один и тот же трюк внутри этих классов, но часто это не так.
источник
В дополнение к тому, что уже было сказано о множественном и виртуальном наследовании, в журнале д-ра Добба есть очень интересная статья: множественное наследование считается полезным
источник
Ты немного запутался. Я не знаю, смешиваете ли вы некоторые понятия.
У вас нет виртуального базового класса в вашем OP. У вас просто есть базовый класс.
Вы сделали виртуальное наследование. Это обычно используется в множественном наследовании, так что несколько производных классов используют члены базового класса, не воспроизводя их.
Базовый класс с чисто виртуальной функцией не создается. для этого требуется синтаксис, который использует Пол. Обычно он используется для того, чтобы производные классы определяли эти функции.
Я не хочу больше об этом объяснять, потому что я не совсем понимаю, что вы спрашиваете.
источник
Это означает, что вызов виртуальной функции будет перенаправлен в «правильный» класс.
C ++ FAQ Lite FTW.
Короче говоря, он часто используется в сценариях множественного наследования, где формируется «алмазная» иерархия. Виртуальное наследование разрушит неоднозначность, созданную в нижнем классе, когда вы вызываете функцию в этом классе, и функция должна быть разрешена либо в класс D1, либо в D2 выше этого нижнего класса. См. Пункт часто задаваемых вопросов для диаграммы и деталей.
Он также используется в сестринском делегировании , мощная функция (хотя и не для слабонервных). Смотрите этот FAQ.
Также см. Пункт 40 в Effective C ++ 3-е издание (43 в 2-е издание).
источник
Пример использования бриллиантового наследования
В этом примере показано, как использовать виртуальный базовый класс в типичном сценарии: для решения проблемы наследования алмазов.
источник
assert(A::aDefault == 0);
из основной функции выдает ошибку компиляции:aDefault is not a member of A
использование gcc 5.4.0. Что это должно делать?Виртуальные классы не виртуальное наследование. Виртуальные классы, которые вы не можете создать, виртуальное наследование - это совсем другое.
Википедия описывает это лучше, чем я. http://en.wikipedia.org/wiki/Virtual_inheritance
источник
Регулярное наследование
В типичном 3-х уровневом наследовании без виртуального наследования без виртуального наследования, когда вы создаете новый самый производный объект, вызывается новый, и требуемый для объекта размер определяется компилятором из типа класса и передается новому.
новый имеет подпись:
И звонит
malloc
, возвращая пустой указательЗатем он передается конструктору самого производного объекта, который немедленно вызовет средний конструктор, а затем средний конструктор немедленно вызовет базовый конструктор. Затем база сохраняет указатель на свою виртуальную таблицу в начале объекта, а затем его атрибуты после него. Затем он возвращается к среднему конструктору, который будет хранить свой указатель виртуальной таблицы в том же месте, а затем его атрибуты после атрибутов, которые были бы сохранены базовым конструктором. Он возвращает наиболее производный конструктор, который хранит указатель на свою виртуальную таблицу в том же месте, а затем его атрибуты после атрибутов, которые были бы сохранены средним конструктором.
Поскольку указатель виртуальной таблицы перезаписывается, указатель виртуальной таблицы всегда оказывается одним из самых производных классов. Виртуальность распространяется на самый производный класс, поэтому, если функция является средней в среднем классе, она будет виртуальной в самом производном классе, но не в базовом классе. Если вы полиморфно приводите экземпляр самого производного класса к указателю на базовый класс, то компилятор не разрешит это косвенным вызовом виртуальной таблицы и вместо этого вызовет функцию напрямую
A::function()
. Если функция является виртуальной для типа, к которому вы ее приводите, она преобразуется в вызов виртуальной таблицы, которая всегда будет функцией самого производного класса. Если он не является виртуальным для этого типа, он просто вызоветType::function()
и передаст на него указатель объекта, приведенный к типу.На самом деле, когда я говорю указатель на его виртуальную таблицу, на самом деле это всегда смещение 16 в виртуальной таблице.
virtual
не требуется снова в более производных классах, если он виртуален в менее производном классе, потому что он распространяется. Но его можно использовать, чтобы показать, что функция действительно является виртуальной функцией, без необходимости проверять классы, которые она наследует определения типов.override
это еще одна защита компилятора, которая говорит, что эта функция что-то переопределяет, а если нет, выдает ошибку компилятора.= 0
означает, что это абстрактная функцияfinal
предотвращает повторную реализацию виртуальной функции в более производном классе и гарантирует, что виртуальная таблица самого производного класса содержит конечную функцию этого класса.= default
в документации четко указано, что компилятор будет использовать реализацию по умолчанию= delete
выдает ошибку компилятора при попытке вызова этогоВиртуальное Наследование
Рассматривать
Практически не наследуя класс баса, вы получите объект, который выглядит следующим образом:
Вместо этого:
Т.е. будет 2 базовых объекта.
В описанной выше ситуации виртуального наследования алмазов после вызова new он вызывает самый производный конструктор, и в этом конструкторе он вызывает все 3 производных конструктора, передавая смещения в таблицу виртуальных таблиц, вместо вызова только вызова
DerivedClass1::DerivedClass1()
иDerivedClass2::DerivedClass2()
затем те , как призваниеBase::Base()
Следующее все скомпилировано в режиме отладки -O0, поэтому будет избыточная сборка
Он вызывает
Base::Base()
с указателем на смещение объекта 32. Base хранит указатель на свою виртуальную таблицу по адресу, который он получает, и его членам после него.DerivedDerivedClass::DerivedDerivedClass()
затем вызываетDerivedClass1::DerivedClass1()
с указателем на объект смещения 0, а также передает адресVTT for DerivedDerivedClass+8
DerivedDerivedClass::DerivedDerivedClass()
затем передает адрес объекта + 16 и адрес для VTTDerivedDerivedClass+24
кDerivedClass2::DerivedClass2()
которой сборка идентичнаDerivedClass1::DerivedClass1()
за исключением того линии ,mov DWORD PTR [rax+8], 3
которые , очевидно , имеет 4 вместо 3 дляd = 4
.После этого он заменяет все 3 указателя виртуальных таблиц в объекте указателями на смещения в
DerivedDerivedClass
vtable таблицы представления этого класса.d->VirtualFunction();
:d->DerivedCommonFunction();
:d->DerivedCommonFunction2();
:d->DerivedDerivedCommonFunction();
:((DerivedClass2*)d)->DerivedCommonFunction2();
:((Base*)d)->VirtualFunction();
:источник