Как я упоминал в комментариях, некоторые проблемы связаны с тем, как обычно реализуется динамическая диспетчеризация.
Предполагая подход 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
, он просто копирует любую запись, найденную в виртуальной таблице B
s, для слота №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
(в простом случае, когда в определении D
s больше ничего не указано, например, использовать B
s foo
или переопределять foo
):
vtable_MI_D
Slot # Method
2 D.bar
3 D.baz
Теперь давайте введем A
и зададим foo
, вернувшись к классическому ромбовидному узору. Итак, A
s 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
B
sfoo
или переопределение вD
. Но большинство описаний этого типа проблем не предполагают никакого дальнейшего переопределения вD
, так что вы точно знаете, какой метод вы вызываете. - person Damien_The_Unbeliever   schedule 24.10.2017B.foo
или переопределениеB.foo
в дополнительном производном классе. Вы не будете искать метод под названиемfoo
- person Damien_The_Unbeliever   schedule 24.10.2017vtables
, как и вы? - person Damien_The_Unbeliever   schedule 24.10.2017