Что такое виртуальный базовый класс в C ++?

Я хочу знать, что такое « виртуальный базовый класс » и что это значит.

Позвольте мне показать пример:

class Foo
{
public:
    void DoSomething() { /* ... */ }
};

class Bar : public virtual Foo
{
public:
    void DoSpecific() { /* ... */ }
};
22.08.2008 01:13:15
мы должны использовать виртуальные базовые классы в «множественном наследовании», потому что, если класс A имеет переменную-член int a, а класс B также имеет член int a, а класс c наследует класс A и B, как мы решаем, какое «a» использовать?
Namit Sinha 1.05.2014 19:01:43
@NamitSinha нет, виртуальное наследование не решает эту проблему. В любом случае член А был бы неоднозначным
Ichthyo 8.12.2017 20:18:57
@NamitSinha Виртуальное наследование не является волшебным инструментом для устранения неоднозначностей, связанных с множественным наследованием. Это «решает» «проблему» наличия косвенной базы более одного раза. Что является проблемой только в том случае, если оно предназначено для совместного использования (часто, но не всегда).
curiousguy 22.12.2019 08:20:21
10 ОТВЕТОВ
РЕШЕНИЕ

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

Рассмотрим следующий сценарий:

class A { public: void Foo() {} };
class B : public A {};
class C : public A {};
class D : public B, public C {};

Приведенная выше иерархия классов приводит к «страшному алмазу», который выглядит следующим образом:

  A
 / \
B   C
 \ /
  D

Экземпляр D будет состоять из B, который включает в себя A, и C, который также включает в себя A. Таким образом, у вас есть два «экземпляра» (для лучшего выражения) из A.

Когда у вас есть этот сценарий, у вас есть возможность двусмысленности. Что происходит, когда вы делаете это:

D d;
d.Foo(); // is this B's Foo() or C's Foo() ??

Виртуальное наследование призвано решить эту проблему. Когда вы указываете virtual при наследовании ваших классов, вы сообщаете компилятору, что вам нужен только один экземпляр.

class A { public: void Foo() {} };
class B : public virtual A {};
class C : public virtual A {};
class D : public B, public C {};

Это означает, что в иерархию включен только один «экземпляр» A. следовательно

D d;
d.Foo(); // no longer ambiguous

Это мини-резюме. Для получения дополнительной информации, прочитайте это и это . Хороший пример также доступен здесь .

527
23.01.2020 11:02:56
@ Bohdan нет, это не так :)
OJ. 28.09.2013 01:21:30
@OJ. почему бы и нет? Они веселые :)
Bohdan 27.12.2013 20:27:18
@Bohdan использует виртуальное ключевое слово столько же, сколько и меньше, потому что когда мы используем виртуальное ключевое слово, применяется механизм большого веса. Таким образом, эффективность вашей программы будет снижена.
Sagar 2.02.2014 14:05:28
Ваша диаграмма «страшный бриллиант» сбивает с толку, хотя, кажется, она широко используется. На самом деле это диаграмма, показывающая отношения наследования классов, а не макет объекта. Заблуждение заключается в том, что если мы используем virtual, то расположение объекта выглядит как ромб; и если мы не используем, virtualто расположение объекта выглядит как древовидная структура, которая содержит два As
M.M 23.07.2015 04:19:19
Я должен понизить этот ответ по причине, изложенной ММ - диаграмма выражает противоположность посту.
David Stone 2.10.2016 17:47:04

Это означает, что вызов виртуальной функции будет перенаправлен в «правильный» класс.

C ++ FAQ Lite FTW.

Короче говоря, он часто используется в сценариях множественного наследования, где формируется «алмазная» иерархия. Виртуальное наследование затем разрушит неоднозначность, созданную в нижнем классе, когда вы вызываете функцию в этом классе, и функция должна быть разрешена в классе D1 или D2 выше этого нижнего класса. Смотрите пункт FAQ для диаграммы и деталей.

Это также используется в делегировании сестры , мощная функция (хотя и не для слабонервных). Смотрите этот FAQ.

Также см. Пункт 40 в Effective C ++ 3-е издание (43 в 2-е издание).

1
23.02.2015 11:58:23

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

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

10
22.08.2008 01:47:36

Ты немного сбиваешь с толку. Я не знаю, смешиваете ли вы некоторые понятия.

У вас нет виртуального базового класса в вашем OP. У вас просто есть базовый класс.

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

Базовый класс с чисто виртуальной функцией не создается. для этого требуется синтаксис, который использует Пол. Обычно он используется для того, чтобы производные классы определяли эти функции.

Я не хочу больше об этом объяснять, потому что я не совсем понимаю, что вы спрашиваете.

1
22.08.2008 01:48:04
«Базовый класс», который используется в виртуальном наследовании, становится «виртуальным базовым классом» (в контексте этого точного наследования).
Luc Hermitte 22.09.2008 01:15:29

Виртуальные классы - это не виртуальное наследование. Виртуальные классы, которые вы не можете создать, виртуальное наследование - это совсем другое.

Википедия описывает это лучше, чем я. http://en.wikipedia.org/wiki/Virtual_inheritance

0
22.08.2008 01:52:04
В C ++ нет такого понятия, как «виртуальные классы». Однако существуют «виртуальные базовые классы», которые являются «виртуальными» в отношении данного наследования. То, что вы имеете в виду, это то, что официально называется «абстрактные классы».
Luc Hermitte 22.09.2008 00:56:14
@LucHermitte, в C ++ определенно есть виртуальные классы. Проверьте это: en.wikipedia.org/wiki/Virtual_class .
Rafid 1.11.2014 22:05:21
msgstr "ошибка:" виртуальная "может быть указана только для функций". Я не знаю, что это за язык. Но в C ++ нет такого понятия, как виртуальный класс .
Luc Hermitte 2.11.2014 23:27:27

Я хотел бы добавить к добрым разъяснениям OJ.

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

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

   B
  / \
D11 D12
 |   |
D21 D22
 \   /
  DD

Ни один из классов не наследует виртуально, все наследуют публично. Классы D21 и D22 будут затем скрывать виртуальную функцию f (), которая неоднозначна для DD, возможно, путем объявления функции частной. Каждый из них определил бы функцию-обертку, f1 () и f2 () соответственно, каждый из которых вызывал бы локальный класс (private) f (), таким образом разрешая конфликты. Класс DD вызывает f1 (), если он хочет D11 :: f () и f2 (), если он хочет D12 :: f (). Если вы определите встроенные оболочки, вы, вероятно, получите около нуля накладных расходов.

Конечно, если вы можете изменить D11 и D12, вы можете сделать один и тот же трюк внутри этих классов, но часто это не так.

7
13.04.2017 08:13:15
Это не вопрос более-менее элегантного или разрешения неясностей (для этого всегда можно использовать явные спецификации xxx ::). При использовании невиртуального наследования каждый экземпляр класса DD имеет два независимых экземпляра B. Как только у класса есть один элемент не статических данных, виртуальное и не виртуальное наследование отличаются не только синтаксисом.
user3489112 25.06.2014 18:02:28
@ user3489112 Как только ... ничего. Виртуальное и не виртуальное наследование отличаются семантически, точка.
curiousguy 27.11.2018 23:44:34

О расположении памяти

Как примечание, проблема с Dreaded Diamond заключается в том, что базовый класс присутствует несколько раз. Таким образом, с регулярным наследованием, вы полагаете, что имеете:

  A
 / \
B   C
 \ /
  D

Но в макете памяти у вас есть:

A   A
|   |
B   C
 \ /
  D

Это объясняет, почему при звонке у D::foo()вас возникает проблема неоднозначности. Но настоящая проблема возникает, когда вы хотите использовать переменную-член A. Например, скажем, у нас есть:

class A
{
    public :
       foo() ;
       int m_iValue ;
} ;

Когда вы будете пытаться получить доступ 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% вероятность, что что-то не так с этой иерархией.

248
21.01.2020 10:40:55
Ваше «что может пойти не так» связано с прямым доступом к базовому члену, а не с множественным наследованием. Избавьтесь от «B», и у вас возникнет та же проблема. Основное правило: «если оно не является частным, оно должно быть виртуальным», то устраняет проблему. M_iValue не является виртуальным и поэтому должно быть частным
Chris Dodd 17.12.2009 15:32:48
@ Крис Додд: Не совсем. То, что происходит с m_iValue, произошло бы с любым символом ( например, typedef, переменная-член, функция-член, приведение к базовому классу и т . Д. ). Это действительно проблема множественного наследования, проблема, которую пользователи должны знать, чтобы правильно использовать множественное наследование, вместо того, чтобы идти по пути Java, и сделать вывод: «Многократное наследование - это 100% зло, давайте сделаем это с интерфейсами».
paercebal 30.10.2010 06:38:59
Привет, Когда мы используем виртуальное ключевое слово, будет только одна копия А. Мой вопрос: как мы узнаем, идет ли оно из B или C? Мой вопрос действителен вообще?
user875036 29.06.2014 18:40:58
@ user875036: A приходит как из B, так и из C. Действительно, виртуальность меняет несколько вещей (например, D вызовет конструктор A, а не B или C). И B, и C (и D) имеют указатель на A.
paercebal 30.06.2014 16:44:40
FWIW, если кому-то интересно, переменные-члены не могут быть виртуальными - виртуальный является спецификатором для функций . SO ссылка: stackoverflow.com/questions/3698831/…
rholmes 23.09.2016 19:21:22

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

4
22.09.2008 00:58:41

Для объяснения множественного наследования с помощью виртуальных баз требуется знание объектной модели C ++. И объяснение темы лучше всего сделать в статье, а не в поле для комментариев.

Наилучшим понятным объяснением, которое я нашел и которое разрешило все мои сомнения по этому вопросу, была эта статья: http://www.phpcompiler.org/articles/virtualinheritance.html

Вам действительно не нужно будет читать что-либо еще по этой теме (если вы не писатель компилятора) после прочтения этого ...

33
14.07.2016 18:14:56

Пример использования бриллиантового наследования

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

#include <cassert>

class A {
    public:
        A(){}
        A(int i) : i(i) {}
        int i;
        virtual int f() = 0;
        virtual int g() = 0;
        virtual int h() = 0;
};

class B : public virtual A {
    public:
        B(int j) : j(j) {}
        int j;
        virtual int f() { return this->i + this->j; }
};

class C : public virtual A {
    public:
        C(int k) : k(k) {}
        int k;
        virtual int g() { return this->i + this->k; }
};

class D : public B, public C {
    public:
        D(int i, int j, int k) : A(i), B(j), C(k) {}
        virtual int h() { return this->i + this->j + this->k; }
};

int main() {
    D d = D(1, 2, 4);
    assert(d.f() == 3);
    assert(d.g() == 5);
    assert(d.h() == 7);
}
1
20.01.2017 12:59:25
assert(A::aDefault == 0);из основной функции выдает ошибку компиляции: aDefault is not a member of Aиспользование gcc 5.4.0. Что это должно делать?
SebNag 20.01.2017 11:57:39
@SebTu ах спасибо, просто что-то я забыл удалить из копии вставить, удалил это сейчас. Пример все еще должен быть значимым без него.
Ciro Santilli 冠状病毒审查六四事件法轮功 20.01.2017 13:00:02