Вопросы множественного наследования и полиморфизма

Рассмотрим этот код С++:

#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, С#...).

Однако есть случаи, когда это может облегчить задачу, особенно в развитии библиотеки.