Так же как и виртуальные функции, множественное наследование — это мощный инструмент, и применять его нужно аккуратно. Основная проблема, возникающая при множественном наследовании, — неоднозначность (см. п. 10.2 в [1]), которая проявляется в двух видах: в виде неоднозначности данных и в виде неоднозначности методов. Пусть, например, в показанном примере (см. листинг 10.1) в защищенной (чтобы наследник мог пользоваться) части класса В1 тоже определено поле s. Тип его может быть каким угодно, не обязательно строковым. Тогда при трансляции возникает конфликт имен в функции pri nt () класса D — компилятор не знает, какое из полей используется.
Аналогичная картина с методами. Например, пусть в базовых классах В1 и В 2 определен некоторый метод:
void f() { /*...*/ }
Класс D унаследует обе версии этого метода, и при попытке вызвать f () возникает ошибка неоднозначности.
Ситуация еще более усугубляется, если у нас в программе наблюдается так называемое «ромбовидное» (рис. 10.1).
class А { /*...*/ }:
class Bl: public А { /*...*/ };
class В2: public А { /*...*/ };
class D: public Bl, public B2 { /*...*/ };
В этом случае класс D получает по 2 экземпляра всех полей и всех методов общего предка — класса А. В этом легко убедиться с помощью функции sizeof ().
Но в таком виде проблема неоднозначности относительно легко разрешается с помощью префикса. Например, в функции вывода print() достаточно явно указать, какое поле мы собираемся выводить: Bl: : s или В2 : : s. Вызов функции f () тоже квалифицируется префиксом: Bl: : f () или В2: : f ().
С происходит то же самое. Пусть в обоих базовых классах определена виртуальная функция print(). Класс-потомок унаследует обе. Если в наследнике не определена собственная виртуальная функция с таким же прототипом, то следующий вызов является неоднозначным:
D d(l,,,l-l\ 1.2); d.print ():
То есть вызов виртуальной функции тоже должен быть квалифицирован именем класса: Bl: :print() или В2: :print(). Однако в таком случае вызов перестает быть виртуальным!
Таким образом, мы видим, что множественное наследование создает достаточно много проблем, если его применять, не думая о последствиях. Однако «правильное» применение бывает чрезвычайно полезным (например, реализация паттерна Adapter). Многие авторы [2, 21, 23, 25] советуют применять множественное наследование только для классов-интерфейсов, не содержащих полей. Именно такое решение принято в языках и : для интерфейсов разрешено множественное наследование, а для — только одиночное. |