Я хочу знать, что такое «виртуальный базовый класс» и что оно означает.
Приведу пример:
class Foo
{
public:
void DoSomething() { /* ... */ }
};
class Bar : public virtual Foo
{
public:
void DoSpecific() { /* ... */ }
};
@NamitSinha нет, виртуальное наследование нет решает эту проблему. Член a в любом случае будет неоднозначным
@NamitSinha Виртуальное наследование - не волшебный инструмент для устранения неоднозначностей, связанных с множественным наследованием. Это «решает» «проблему» наличия косвенной базы более одного раза. Что является проблемой только в том случае, если он был предназначен для совместного использования (часто, но не всегда).





Это означает, что вызов виртуальной функции будет перенаправлен в «правильный» класс.
C++ FAQ Lite FTW.
Короче говоря, он часто используется в сценариях множественного наследования, где формируется «ромбовидная» иерархия. Виртуальное наследование затем устранит неоднозначность, созданную в нижнем классе, когда вы вызываете функцию в этом классе, и функция должна быть разрешена либо в класс D1, либо в D2 выше этого нижнего класса. См. Схему и подробности в Пункт FAQ.
Он также используется в родственная делегация, мощной функции (но не для слабонервных). См. FAQ по это.
Также см. Пункт 40 в Эффективном C++ 3-м издании (43 во 2-м издании).
Виртуальные базовые классы, используемые в виртуальном наследовании, являются способом предотвращения появления нескольких «экземпляров» данного класса в иерархии наследования при использовании множественного наследования.
Рассмотрим следующий сценарий:
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
Это краткое изложение. Для получения дополнительной информации прочтите это и это. Также доступен хороший пример здесь.
@Bohdan нет, это не так :)
@OJ. почему нет? Они веселые :)
а если серьезно, какие бывают ситуации, когда мы не будем использовать виртуальные?
@Bohdan использует ключевое слово virtual как можно меньше, потому что, когда мы используем ключевое слово virtual, применяется тяжелый механизм. Таким образом, эффективность вашей программы снизится.
@OJ. Просто хочу знать еще одну вещь, после использования ключевого слова virtual, какой экземпляр называется d.Foo() // is this B's Foo() or C's Foo() (after using virtual keyword)
@Viktor, это не важно, это не B или C. Просто подумайте, что это объединенный метод, который на самом деле принадлежит grandfather A
Добавление ключевого слова virtual для класса D перед B, C или обоими сразу не решает эту проблему. codepad.org/QPstHhBa У меня всегда получается error: request for member 'Foo' is ambiguous. Итак, как снова использовать ключевое слово virtual?
Что делать, если, набрав d.Foo(), я хочу вызвать Foo() из класса B?
@lavsprat Ты делаешь d.B::foo()
Ваша диаграмма «ужасного ромба» сбивает с толку, хотя кажется, что она широко используется. На самом деле это диаграмма, показывающая отношения наследования классов - нет - макет объекта. Непонятная часть состоит в том, что если мы все же используем virtual, то расположение объекта будет похоже на ромб; и если мы не используем virtual, то макет объекта выглядит как древовидная структура, содержащая два A
Я должен проголосовать против этого ответа по причине, изложенной M.M. - диаграмма выражает противоположность сообщения.
Фантастический ответ. Один вопрос, который не был решен для меня, заключается в том, какая версия A из B или C будет использоваться при использовании виртуального наследования? Можем ли мы это знать? Это тоже неоднозначно, от чего-то зависит, неактуально? Заранее спасибо.
Похоже, это создает еще одну проблему ... что, если бы Foo был виртуальным, а B и C имели разные реализации Foo? Что, черт возьми, должен делать D, когда звонит Foo? Есть ли реальный вариант использования, когда наследование A практически решает что-либо? Я имею в виду, что должно быть, если это было принято в стандарте, верно?
@Viktor, что вы имеете в виду под "тяжеловесным механизмом"?
@Matthias Когда вы вызываете виртуальную функцию, таблица виртуальных функций должна просматриваться перед вызовом метода, тогда как невиртуальные функции вызываются немедленно.
То же, что и вопрос @ Nik-Lz, какой Foo будет называться, B или C?
A virtual base class is a class that cannot be instantiated : you cannot create direct object out of it.
Я думаю, вы путаете две очень разные вещи. Виртуальное наследование - это не то же самое, что абстрактный класс. Виртуальное наследование изменяет поведение вызовов функций; иногда он разрешает вызовы функций, которые в противном случае были бы неоднозначными, иногда он откладывает обработку вызовов функций классу, отличному от того, который можно было бы ожидать при невиртуальном наследовании.
Вы немного запутались. Не знаю, путаете ли вы какие-то концепции.
У вас нет виртуального базового класса в вашем OP. У вас просто базовый класс.
Вы сделали виртуальное наследование. Обычно это используется при множественном наследовании, так что несколько производных классов используют члены базового класса, не воспроизводя их.
Базовый класс с чистой виртуальной функцией не создается. для этого требуется синтаксис, к которому приходит Пол. Обычно он используется для того, чтобы производные классы определяли эти функции.
Я не хочу больше объяснять это, потому что я не совсем понимаю, о чем вы спрашиваете.
«Базовый класс», который используется в виртуальном наследовании, становится «виртуальным базовым классом» (в контексте этого точного наследования).
Виртуальные классы нет такие же, как виртуальное наследование. Виртуальные классы, которые вы не можете создать, виртуальное наследование - это совсем другое.
Википедия описывает это лучше, чем я. http://en.wikipedia.org/wiki/Virtual_inheritance
В C++ нет такого понятия, как «виртуальные классы». Однако существуют «виртуальные базовые классы», которые являются «виртуальными» в отношении данного наследования. Вы имеете в виду то, что официально называется «абстрактными классами».
@LucHermitte, в C++ определенно есть виртуальные классы. Проверьте это: en.wikipedia.org/wiki/Virtual_class.
«ошибка:« виртуальный »можно указывать только для функций». Я не знаю, что это за язык. Но в C++ категорически не существует такой вещи, как виртуальный класс.
Это всего лишь случай, когда люди используют терминологию Java для описания C++. Я часто сталкивался с этим в университете, где традиционно преподают Java, а не C++. Я прошел курс C++ (но отказался от Java), и он раньше приводил меня в чувство, когда я слышал «виртуальный», а не «абстрактный».
Я хотел бы добавить к любезным разъяснениям О.Джея.
Виртуальное наследование не обходится без платы. Как и во всем виртуальном, вы получаете удар по производительности. Есть способ обойти это снижение производительности, возможно, менее элегантный.
Вместо того, чтобы разбивать алмаз виртуально, вы можете добавить еще один слой к алмазу, чтобы получить что-то вроде этого:
B
/ \
D11 D12
| |
D21 D22
\ /
DD
Ни один из классов не наследуется виртуально, все наследуются публично. Классы D21 и D22 затем скроют виртуальную функцию f (), что неоднозначно для DD, возможно, объявив функцию частной. Каждый из них определяет функцию-оболочку, f1 () и f2 () соответственно, каждый из которых вызывает локальный класс (частный) f (), тем самым разрешая конфликты. Класс DD вызывает f1 (), если он хочет D11 :: f () и f2 (), если он хочет D12 :: f (). Если вы определите встроенные обертки, вы, вероятно, получите нулевые накладные расходы.
Конечно, если вы можете изменить D11 и D12, вы можете проделать тот же трюк внутри этих классов, но часто это не так.
Это не вопрос более или менее элегантного или разрешения двусмысленности (для этого вы всегда можете использовать явные xxx :: спецификации). При невиртуальном наследовании каждый экземпляр класса DD имеет независимые от два экземпляры B. Как только класс имеет один нестатический член данных, виртуальное и невиртуальное наследование отличаются не только синтаксисом.
@ user3489112 Как только ... ничего. Виртуальное и не виртуальное наследование семантически различаются, точка.
Кстати, проблема с 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% вероятность того, что вы сделали что-то не так с указанной иерархией.
Ваше «что может пойти не так» связано с прямым доступом к базовому члену, а не с множественным наследованием. Избавьтесь от "B", и у вас возникнет та же проблема. Основное правило: "если он не приватный, он должен быть виртуальным" позволяет избежать проблемы. M_iValue не является виртуальным и поэтому должен быть приватным
@ Крис Додд: Не совсем так. То, что происходит с m_iValue, произошло бы с любым символом (например typedef, переменная-член, функция-член, приведение к базовому классу и т. д.). Это действительно проблема множественного наследования, проблема, о которой пользователи должны знать, чтобы правильно использовать множественное наследование, вместо того, чтобы идти по пути Java и заключать: «Множественное наследование - это 100% зло, давайте сделаем это с интерфейсами».
Привет, когда мы используем виртуальное ключевое слово, будет только одна копия A. Мой вопрос: как мы узнаем, исходит ли оно от B или C? Действительно ли мой вопрос вообще актуален?
@ user875036: A поступает как от B, так и от C. Действительно, виртуальность меняет несколько вещей (например, D будет вызывать конструктор A, а не B или C). И B, и C (и D) имеют указатель на A.
«это означает, что у вас есть 95%, чтобы сделать что-то не так с указанной иерархией». просто из любопытства, как вы получили число 95%?
@newbie: Почему? Невидимый кролик на моем плече сказал мне ... :-) ... Если серьезно, это предположение из (небольшого) количества множественного наследования реализаций (MEI), которое я видел за свою карьеру. По умолчанию нужно опасаться MEI, не запрещая его навсегда в коде C++. Итак, мы должны изучить базовые классы, спросить себя, желательно ли наследование и / или не лучше ли композиция. В конце концов, если дизайн MEI проходит все тесты и удовлетворительно отвечает на все вопросы, то это правильный дизайн, и мы должны его использовать.
FWIW, если кому-то интересно, переменные-члены не можешь будут виртуальными - virtual - это спецификатор для функции. Ссылка SO: stackoverflow.com/questions/3698831/…
В дополнение к тому, что уже было сказано о множественном и виртуальном наследовании, в журнале доктора Добба есть очень интересная статья: Множественное наследование считается полезным
Для объяснения множественного наследования с помощью виртуальных баз требуется знание объектной модели C++. И четкое объяснение темы лучше всего делать в статье, а не в поле для комментариев.
Лучшее, читаемое объяснение, которое я нашел, разрешившее все мои сомнения по этому поводу, - это статья: http://www.phpcompiler.org/articles/virtualinheritance.html
Вам действительно не нужно будет больше ничего читать по этой теме (если вы не являетесь автором компилятора) после прочтения этого ...
Пример использования исполняемого алмазного наследования
В этом примере показано, как использовать виртуальный базовый класс в типичном сценарии: для решения проблем наследования алмаза.
Рассмотрим следующий рабочий пример:
main.cpp
#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);
}
Скомпилируйте и запустите:
g++ -ggdb3 -O0 -std=c++11 -Wall -Wextra -pedantic -o main.out main.cpp
./main.out
Если мы удалим virtual в:
class B : public virtual A
мы получили бы стену ошибок о том, что GCC не может разрешить элементы и методы D, которые были унаследованы дважды через A:
main.cpp:27:7: warning: virtual base ‘A’ inaccessible in ‘D’ due to ambiguity [-Wextra]
27 | class D : public B, public C {
| ^
main.cpp: In member function ‘virtual int D::h()’:
main.cpp:30:40: error: request for member ‘i’ is ambiguous
30 | virtual int h() { return this->i + this->j + this->k; }
| ^
main.cpp:7:13: note: candidates are: ‘int A::i’
7 | int i;
| ^
main.cpp:7:13: note: ‘int A::i’
main.cpp: In function ‘int main()’:
main.cpp:34:20: error: invalid cast to abstract class type ‘D’
34 | D d = D(1, 2, 4);
| ^
main.cpp:27:7: note: because the following virtual functions are pure within ‘D’:
27 | class D : public B, public C {
| ^
main.cpp:8:21: note: ‘virtual int A::f()’
8 | virtual int f() = 0;
| ^
main.cpp:9:21: note: ‘virtual int A::g()’
9 | virtual int g() = 0;
| ^
main.cpp:34:7: error: cannot declare variable ‘d’ to be of abstract type ‘D’
34 | D d = D(1, 2, 4);
| ^
In file included from /usr/include/c++/9/cassert:44,
from main.cpp:1:
main.cpp:35:14: error: request for member ‘f’ is ambiguous
35 | assert(d.f() == 3);
| ^
main.cpp:8:21: note: candidates are: ‘virtual int A::f()’
8 | virtual int f() = 0;
| ^
main.cpp:17:21: note: ‘virtual int B::f()’
17 | virtual int f() { return this->i + this->j; }
| ^
In file included from /usr/include/c++/9/cassert:44,
from main.cpp:1:
main.cpp:36:14: error: request for member ‘g’ is ambiguous
36 | assert(d.g() == 5);
| ^
main.cpp:9:21: note: candidates are: ‘virtual int A::g()’
9 | virtual int g() = 0;
| ^
main.cpp:24:21: note: ‘virtual int C::g()’
24 | virtual int g() { return this->i + this->k; }
| ^
main.cpp:9:21: note: ‘virtual int A::g()’
9 | virtual int g() = 0;
| ^
./main.out
Проверено на GCC 9.3.0, Ubuntu 20.04.
assert(A::aDefault == 0); из основной функции дает мне ошибку компиляции: aDefault is not a member of A с использованием gcc 5.4.0. Что ему делать?
@SebTu ах, спасибо, просто кое-что, что я забыл удалить из копипаста, удалил это сейчас. Без него пример все равно должен иметь смысл.
должны ли мы использовать виртуальные базовые классы в «множественном наследовании», потому что, если класс A имеет переменную-член int a, а класс B также имеет член int a, а класс c наследует класс A и B, как нам решить, какой «a» использовать?