Ромбовидное и треугольное наследование

Мой вопрос такой же, как и этот старый, но я еще не понимаю полученный ответ: Diamond Problem

В задаче о алмазе D наследуется от B и C, которые оба наследуют от A, и B и C переопределяют метод foo в A.

Предположим вместо этого треугольник: нет A, D наследуется от B и C, которые оба реализуют метод foo.

Насколько я понимаю, без специальной обработки следующее будет неоднозначным в либо ромбе, либо треугольнике, потому что неясно, какой foo вызывать:

D d = new D;
d.foo();

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


person allstar    schedule 24.10.2017    source источник
comment
Я не уверен, что понимаю двусмысленность во втором примере - вы будете вызывать метод Bs foo или переопределение в D. Но большинство описаний этого типа проблем не предполагают никакого дальнейшего переопределения в D, так что вы точно знаете, какой метод вы вызываете.   -  person Damien_The_Unbeliever    schedule 24.10.2017
comment
Я думал, что если язык ведет себя таким образом, что вызывает метод на основе типа объекта во время выполнения, поэтому, когда объект имеет тип D, все равно будет неоднозначно, вызывать ли B.foo() или C.foo(). Независимо от того, обрабатывается ли он таким образом или так, как вы описываете, кажется, что это что-то общее с ромбом и треугольником.   -  person allstar    schedule 24.10.2017
comment
Поиск имени происходит во время компиляции в большинстве языков. Во втором примере во время выполнения вы будете искать B.foo или переопределение B.foo в дополнительном производном классе. Вы не будете искать метод под названием foo   -  person Damien_The_Unbeliever    schedule 24.10.2017
comment
Я считаю, что в Java это не так из-за поздней привязки. Только что провел небольшой тест с C, унаследованным от B. B b = new C(); C c = новый C(); ... В обоих случаях b.foo() и c.foo() вызывают метод класса C.   -  person allstar    schedule 24.10.2017
comment
Java не допускает множественного наследования.   -  person DodgyCodeException    schedule 24.10.2017
comment
Я думаю, что вам не хватает того, как обычно реализуется динамическая диспетчеризация, что я пытался описать выше, но вы не поняли, так как ваши контрпримеры говорят вразрез с тем, что я' м пытаюсь сказать. Ответ, вероятно, будет слишком большим, если вы не понимаете, например. что такое vtables, как и вы?   -  person Damien_The_Unbeliever    schedule 24.10.2017
comment
А, я неправильно прочитал то, что вы написали. Вы правы, я не знаю, что такое vtables в глубине, но теперь я понимаю, каков ваш первоначальный комментарий. В моем примере я переопределял foo в C, что, как вы указываете, не является типичным описанием проблемы. Я обновил вопрос, чтобы удалить этот неверный пример.   -  person allstar    schedule 24.10.2017
comment
@Damien_The_Unbeliever относительно случая, который я оставил в вопросе, похоже, что это связано с множественным наследованием в более общем смысле, а не конкретно с множественным наследованием от двух классов с одним и тем же суперклассом (алмазом).   -  person allstar    schedule 24.10.2017


Ответы (2)


Как я упоминал в комментариях, некоторые проблемы связаны с тем, как обычно реализуется динамическая диспетчеризация.

Предполагая подход vtable, любой конкретный тип должен иметь возможность создавать vtable, которая позволяет рассматривать его как самого себя или любой из его супертипов. При одиночном наследовании это может быть очень легко реализовано, поскольку виртуальная таблица каждого типа может начинаться с того же макета виртуальной таблицы, что и его непосредственный супертип, за которым следуют любые новые члены, которые он вводит.

Например. Если B имеет два метода

vtable_B
Slot #       Method
1            B.foo
2            B.bar

И D наследуется от B, переопределяет bar и вводит baz:

vtable_SI_D
Slot #       Method
1            B.foo
2            D.bar
3            D.baz

Поскольку D не переопределяет foo, он просто копирует любую запись, найденную в виртуальной таблице Bs, для слота №1.

Тогда любой код, работающий с переменной от D до B, будет использовать только слоты №1 и №2, и все будет работать нормально.

Однако при внедрении множественного наследования вы не сможете использовать одну виртуальную таблицу. Предположим, теперь мы вводим C, который также имеет методы foo и bar. Теперь нам нужно использовать разные vtables, когда D преобразуется в B:

vtable_MI_D_as_B
Slot #       Method
1            B.foo
2            D.bar

or to C:

vtable_MI_D_as_C
Slot #       Method
1            C.foo
2            D.bar

Они однозначны1. Проблема заключается в попытке заполнить vtable для D, когда она ни к чему не приводится:

Slot #       Method
1            <what goes here>
2            D.bar
3            D.baz

Итак, вы правы в том, что наследование треугольника действительно вызывает некоторые проблемы. Но поскольку мы используем другую виртуальную таблицу для D как D (в отличие от D как B или C), мы можем просто опустить запись для слота №1 и сделать ее незаконно вызывать D.foo (в простом случае, когда в определении Ds больше ничего не указано, например, использовать Bs foo или переопределять foo):

vtable_MI_D
Slot #       Method
2            D.bar
3            D.baz

Теперь давайте введем A и зададим foo, вернувшись к классическому ромбовидному узору. Итак, As vtable:

vtable_A
Slot #       Method
1            A.foo

B и C такие же, как описано выше. Мы можем следовать точно такому же подходу, описанному выше, для D, за исключением одной дополнительной проблемы. Мы должны предоставить виртуальную таблицу для D, представленную как A. Мы не можем просто пропустить слот № 1 — код, работающий с A, ожидает, что сможет вызвать foo. И мы не можем просто скопировать запись из виртуальной таблицы B или C, поскольку они имеют разные значения и оба являются непосредственными супертипами.

Я полагаю, что в этом суть того, почему обычно используется ромбовидный узор, потому что мы не можем просто реализовать правило "вы не можете вызывать foo для D" и покончить с этим.


1Также стоит отметить, что слоты №1 и №2 в виртуальных таблицах vtable_MI_D_as_B и vtable_MI_D_as_C полностью не связаны между собой. C мог бы иметь слот № 2 для своего метода foo и слот № 6 для своего метода bar. Методы с одинаковыми именами не обязательно будут использовать одни и те же слоты.

Это контрастирует с более поздним обсуждением шаблона наследования ромбов, где слот № 1 действительно является одним и тем же слотом для всех типов.

person Damien_The_Unbeliever    schedule 25.10.2017
comment
Это потрясающее объяснение, большое спасибо. У меня есть продолжение: почему у vtable_MI_D_as_A не будет копии A.foo из vtable_A, аналогично тому, как вы описали vtable_MI_D_as_B - person allstar; 25.10.2017
comment
@allstar - ну, вы могли, но обычно наследование работает не так. В vtable_MI_D_as_B B является непосредственным супертипом, поэтому мы наследуем его реализацию foo, если у нас нет собственной. При одиночном наследовании, но с использованием отдельных виртуальных таблиц для каждого экземпляра as, vtable_SI_D_as_A по-прежнему будет использовать Bs foo. На данный момент любые предлагаемые модификации пытаются исправить проблему наследования бриллиантов - и все эти исправления имеют различные недостатки - это то, что делает эту проблему широко обсуждаемой в первую очередь :-) - person Damien_The_Unbeliever; 26.10.2017

D d = new D();
d.foo();

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

(B)d.foo();
(C)d.foo();

Без проблем. Это применимо независимо от того, наследуют ли A и B A или нет. Проблема в том, когда у вас есть:

A a = new D();
a.foo();

Это также двусмысленно, несмотря на то, что A явно имеет только одну версию foo. Если вам дали A, а оказалось D, вы получите сообщение об ошибке, даже если вы вызываете метод для объекта. Если вам нужно проверить, является ли это D, решите, следует ли привести к B или C, прежде чем вызывать foo, что нарушает полиморфизм

Изменить, чтобы ответить на комментарий:

у меня есть метод

void DoStuff(A a)
{
    a.foo();
}

и я передаю D. Какую версию foo() следует вызывать? Если DoStuff находится в библиотеке, на которую ссылается другой код, который определил B, C и D, DoStuff() не будет знать, что с этим делать, так как вы не можете ожидать, что сопровождающий библиотеки реализует переопределения для каждого возможного объекта, который наследует из

person Jaloopa    schedule 24.10.2017
comment
Верно, но кажется, что одна и та же проблема должна решаться одинаково: (B)a.foo(); - person allstar; 24.10.2017
comment
То есть проблема, которую вы описываете, безусловно, верна, но поскольку она добавляет еще один уровень наследования, а проблемы уже существуют только с множественным наследованием B и C, кажется странным, что каноническая проблема называется алмазом проблема. Я чувствую, что мне не хватает чего-то совершенно другого. - person allstar; 24.10.2017