Вопросы множественного наследования и полиморфизма
Рассмотрим этот код С++:
#include <iostream>
using namespace std;
struct B {
virtual int f() { return 1; }
int g() { return 2; }
};
struct D1 : public B { // (*)
int g() { return 3; }
};
struct D2 : public B { // (*)
virtual int f() { return 4; }
};
struct M : public D1, public D2 {
int g() { return 5; }
};
int main() {
M m;
D1* d1 = &m;
cout << d1->f()
<< static_cast<D2&>(m).g()
<< static_cast<B*>(d1)->g()
<< m.g();
}
Он печатает 1225
. Если мы сделаем виртуальное наследование, т.е. Добавим virtual
до public
в строки, помеченные знаком (*), он печатает 4225
.
- Можете ли вы объяснить, почему
1
изменяется на 4
?
- Можете ли вы объяснить значение
static_cast<D2&>(m)
и static_cast<B*>(d1)
?
- Как вы не теряетесь в таких комбинациях? Вы что-то рисуете?
- Общеизвестно ли распространять такие сложные настройки в обычных проектах?
Ответы
Ответ 1
(1) Можете ли вы объяснить, почему 1 изменяется на 4?
Без наследования virtual
существует 2 независимые иерархии наследования; B->D1->M
и B->D2->M
. Итак, представьте 2 virtual
таблицы функций (хотя это реализация определена).
Когда вы вызываете f()
с D1*
, он просто знает о B::f()
и что он. При наследовании virtual
база class B
делегируется M
и, следовательно, D2::f()
рассматривается как часть class M
.
(2) Можете ли вы объяснить значение static_cast<D2&>(m)
и static_cast<B*>(d1)
?
static_cast<D2&>(m)
, походит на рассмотрение объекта class M
как class D2
static_cast<B*>(d1)
, походит на рассмотрение указателя class D1
как class B1
.
Оба действительны.
Поскольку g()
не virtual
, выбор функции происходит во время компиляции. Если бы это было virtual
, тогда все эти кастинги не будут иметь значения.
(3) Как вы не теряетесь в таких комбинациях? Вы рисуете что-то?
Конечно, это сложно и на первый взгляд, если их так много, можно легко потерять.
(4) Общеизвестно ли выявлять такие сложные настройки в обычных проектах?
Не совсем, это необычный, а иногда и запах кода.
Ответ 2
Картинки говорят громче слов, поэтому перед ответами...
Иерархия класса M БЕЗ виртуального базового наследования B для D1 и D2:
M
/ \
D1 D2
| |
B B
Иерархия класса M С виртуальное базовое наследование B для D1 и D2:
M
/ \
D1 D2
\ /
B
-
Перекрестное делегирование, или, как мне нравится называть это, родственный-полиморфизм с твист. Виртуальное базовое наследование будет исправлять переопределение B:: f() как D2: f(). Надеюсь, что изображение поможет объяснить это, когда вы рассмотрите, где реализованы виртуальные функции, и что они переопределяют в результате цепей наследования.
-
static_cast
использование оператора в этом случае приводит к преобразованию из типов классов производных к базовым.
-
Много опыта чтения действительно плохого кода и знание того, как основы работы языка
-
К счастью, нет. Это не распространено. Исходные библиотеки iostream дали бы вам кошмары, хотя, если это вообще путается.
Ответ 3
Можете ли вы объяснить, почему 1 изменяется на 4?
Почему он меняется на 4
? Из-за cross-delegation.
Здесь граф наследования перед виртуальным наследованием:
B B
| |
D1 D2
\ /
M
d1
- это d1
, поэтому он не знает, что D2
даже существует, а его родительский элемент (B
) не знает, что существует D2
. Единственный возможный результат - вызов B::f()
.
После добавления виртуального наследования базовые классы объединяются вместе.
B
/ \
D1 D2
\ /
M
Здесь, когда вы запрашиваете d1
для f()
, он смотрит на своего родителя. Теперь они имеют один и тот же B
, поэтому B
f()
будет переопределен D2::f()
и вы получите 4
.
Да, это странно, потому что это означает, что d1
удалось вызвать функцию из D2
, о которой ничего не известно. Это одна из наиболее странных частей С++, и ее обычно избегают.
Можете ли вы объяснить значение static_cast (m) и static_cast (d1)?
Что ты не понимаешь? Они набрасывают m
и d1
на D2&
и B*
соответственно.
Как вы не теряетесь в таких комбинациях? Вы рисуете что-то?
Не в этом случае. Это сложно, но достаточно мало, чтобы держать вас в голове. Я привел график в приведенном выше примере, чтобы сделать все как можно более ясным.
Общеизвестно ли выявлять такие сложные настройки в обычных проектах?
Нет. Всем известно, чтобы избежать страшной картины наследования алмазов, потому что она слишком сложна, и обычно есть более простой способ делать то, что вы хотите сделать.
В общем, лучше предпочесть состав над наследованием.
Ответ 4
Этот вопрос представляет собой несколько вопросов:
- Почему функция
virtual
B::f()
не переопределяется при использовании не virtual
наследования? Разумеется, ответ состоит в том, что у вас есть два объекта Base
: один в качестве базы D1
, который переопределяет f()
и один в качестве базы D2
, который не переопределяет f()
. В зависимости от того, какую ветвь вы считаете, что ваш объект вызывается при вызове f()
, вы получите разные результаты. Когда вы меняете настройку, чтобы иметь только один подобъект B
, рассматривается любое переопределение в графе наследования (и если обе ветки переопределяют его, я думаю, что вы получите сообщение об ошибке, если вы не переопределите его в месте объединения веток еще раз.
- Что означает
static_cast<D2&>(m)
? Поскольку из Base
есть две версии f()
, вам нужно выбрать, какой из них вы хотите. С помощью static_cast<D2&>(m)
вы просматриваете M
как объект D2
. Без броска компилятор не сможет определить, к какому из двух объектов вы смотрите, и это приведет к ошибке двусмысленности.
- Что означает
static_cast<B*>(d1)
? Это бывает ненужным, но рассматривает объект только как объект B*
.
Как правило, я стараюсь избегать множественного наследования для всего, что не является тривиальным. Большую часть времени я использую множественное наследование, чтобы воспользоваться преимуществами оптимизации пустой базы или создать что-то с переменным числом членов (подумайте std::tuple<...>
). Я не уверен, что мне когда-либо приходилось сталкиваться с фактической необходимостью использовать множественное наследование для обработки полиморфизма в производственном коде.
Ответ 5
1) Можете ли вы объяснить, почему 1 изменяется на 4?
Без виртуального наследования есть два экземпляра из B
в M
, по одному для каждой ветки этого "алмаза". Одна из кромок алмаза (D2
) переопределяет функцию, а другая (D1
) не работает. Поскольку D1
объявлен как D1
, d1->f()
означает, что вы хотите получить доступ к копии B
, функция которой не была переопределена. Если вы должны были отбросить на D2
, вы получите другой результат.
Используя виртуальное наследование, вы объединяете два экземпляра B
в один, поэтому D2::f
эффективно переопределяет B:f
после создания M
.
2) Можете ли вы объяснить значение static_cast<D2&>(m)
и static_cast<B*>(d1)
?
Они набрасываются на D2&
и B*
соответственно. Поскольку g
не является виртуальным, вызывает вызов B:::g
.
3) Как вы не теряетесь в таких комбинациях? Вы рисуете что-то?
Иногда;)
4) Общеизвестно ли выявлять такие сложные настройки в обычных проектах?
Не слишком распространено. На самом деле есть целые языки, которые получают просто отлично, не говоря уже о многократном виртуальном наследовании (Java, С#...).
Однако есть случаи, когда это может облегчить задачу, особенно в развитии библиотеки.