7.4.
Множественное наследование в C++
Так же, как
язык CLOS представляет собой объектно-ориентированное расширение языка LISP,
так и язык C++ создан на основе широко известного языка С и сохранил все его
возможности, добавив к ним средства объектно-ориентированного программирования.
Если отвлечься от того факта, что CLOS и C++ основаны на разных языках-прототипах,
то основное отличие между ними заключается в реализации механизма наследования,
в частности множественного наследования. В языке C++ множественное наследование
трактуется совсем не так, как мы это делали в предшествующих разделах настоящей
главы, а потому этот вопрос заслуживает подробного обсуждения.
В языке C++
родовые операции реализуются в виде виртуальных функций. Виртуальная
функция, объявленная в классе X, это функция, которая может быть перегружена
(переопределена) в классе, производном от X. При объявлении в классе X виртуальная
функция вообще может не иметь тела, т.е. программного кода реализации.
В таком случае функция называется чисто виртуальной, а класс, имеющий
одну или более чисто виртуальных функций, является абстрактным базовым классом,
экземпляры которого создать невозможно. В любом случае ключевое слово virtual
говорит компилятору, что программный код функции будет уточнен в производных
классах.
Те методы,
которые вызываются на выполнение, являются невиртуальными членами-функциями,
т.е. функциями, имеющими определенный программный код, который не перегружается
в производных классах. В этом смысле C++ существенно отличается от языка CLOS,
в котором практически все функции суперкласса в большей или меньшей степени
модифицируются механизмом наложения методов. Поэтому в C++ существует множество
синтаксических тонкостей, в которых не нуждается CLOS. Например, во всех классах
иерархии виртуальная функция должна иметь квалификатор virtual до тех пор, пока
в некотором производном классе не будет представлена ее конкретная реализация.
В чисто иерархической
структуре классов, когда каждый производный класс имеет единственного "родителя",
передача методов по наследству выполняется совершенно очевидным способом. Единственная
тонкость в реализации этого механизма в C++ состоит в использовании квалификаторов
наследования public и private. Если не вдаваться в подробности, то наследование
вида public представляет собой отношение "is а" (является), которое
мы использовали при обсуждении фреймов. Наследование вида private ближе к отношению
"реализовано в терминах", которое позволяет скрыть определенные детали
реализации интерфейсов объектов. Такое полезное разделение "выпало"
в языке CLOS, в котором каждое отношение "класс-подкласс" несет семантический
смысл.
Но если обратиться
к множественному наследованию, то механизмы его реализации в C++ и CLOS существенно
отличаются. Поскольку в языке C++ не существует такого понятия, как порядок
предшествования классов, то даже такой простой случай, как в рассмотренном выше
примере "Алмаз Никсона", приводит к неоднозначности. Будем считать,
что отношения между классами Person, Quaker, Republican и Republican_Quaker,
как и прежде, имеют вид, представленный на рис. 7.8.
Объявление
классов Person, Quaker, Republican и Republican Quaker на языке C++ показано
в листинге 7.2 (программный код объявления включен в файл nixon.h).
Рис. 7.8.
Отношения между классами в примере "Алмаз Никсона"
Листинг
7.2. Файл nixon.h. Объявление классов, версия 1
//
Объявление классов для задачи "Алмаз Никсона" finclude <iostream.h>
class
Person
{
public:
Personf)
{};
virtual
"Person() {};
virtual
void speak() = 0; };
class
Republican : public Person
{
public:
Republican))
{};
virtual
~Republican)) {};
virtual
void speak() { cout « "War";} };
class
Quaker : public Person
{
public:
Quakerf)
{};
virtual
~Quaker)) {};
virtual
void speak)) { cout « "Peace";} };
class Republican_Quaker : public Republican,
public
Quaker
{
public:
Republican_Quaker()
{};
virtual
~Republican_Quaker() {};
};
Создадим
экземпляр richard класса Republican_Quaker.
#include
"nixon.h" void main))
Republican_Quaker
richard; richard.speak));
При обработке
этого программного кода компилятор C++ обнаружит, что вызов richard.speak))
содержит неоднозначную ссылку. Оно и понятно, поскольку нельзя однозначно заключить,
скажет ли Ричард "War" (война) или "Peace" (мир).
Если мы решим,
что метод speak)) класса Republican_Quaker должен "брать пример" с
класса Quaker, то проблему можно решить, определив этот метод одним из двух
способов:
void
S::speak(){ cout << "Peace"; }
или
void
S::speak)({Quaker::speak)); }
Первый вариант
просто перегружает оба наследованных определения метода, а второй в явном виде
вызывает один из них, а именно тот вариант, который реализован в классе Quaker.
Однако совершенно
незначительное на первый взгляд изменение в файле определения классов может
разительно изменить поведение объекта. Предположим, решено удалить объявления
методов speak)) из всех классов, кроме Person, как это показано в листинге 7.3.
Листинг
7.3. Файл nixon.h. Объявление классов, версия 2
class
Person
public:
Person))
{};
virtual
"Person)) {};
virtual
void speak)){ cout « "Beer";}
};
class
Republican : public Person
public:
Republican))
{}; virtual ~Republican)) {};
class
Quaker : public Person
public:
Quaker))
{};
virtual
~Quaker)) {};
class
Republican Quaker : public Republican, public Quaker
{
public:
Republican_Quaker(
) {} ;
virtual
~Republican_Quaker( ) {};
}
При обработке
такого файла определения компилятор опять выдаст сообщение о неоднозначности
ссылки на метод speak ( ). Это произойдет по той причине, что компилятор сформирует
две копии объявления класса Person — по одной для каждого пути наследования,
а это приведет к конфликту имен. Чтобы устранить эту неоднозначность, нужно
объявить Person как виртуальный базовый класс и для Republican, и для
Quaker. Тогда оба производных класса будут ссылаться на единственный объект
суперкласса (листинг 7.4).
Листинг
7.4. Файл nixon.h. Объявление классов, версия 3
class
Person
{
public:
Per
son () {};
virtual
"Person)) {};
virtual
void speak(){ cout << "Beer";} И
class
Republican : virtual public Person
{
public:
Republican))
{};
virtual
~Republican)) {};
};
class
Quaker : virtual public Person
{
public:
Quaker))
{};
virtual
~Quaker)) .{};
}
class
Republican_Quaker : public Republican, public Quaker
{
public:
Republican_Quaker { ) { } ;
virtual
"Republican_Quaker( ) {};
}
Объявление
Person в качестве виртуального базового класса для Republican и Quaker имеет
и еще одно преимущество. Предположим, что нам нужно сделать так, чтобы класс
Republican_Quaker отдавал предпочтение стилю поведения квакеров, а все другие
были индифферентны к вопросам войны и мира и следовали линии поведения, определенной
классом Person. Тогда, поскольку Person является виртуальным базовым классом,
можно заставить доминировать Quaker::speak)) над Person::speak)) для класса
Republican_Quaker (листинг 7.5).
Листинг
7.5. Файл nixon.h. Объявление классов, версия 4
class
Person
public:
Person))
{};
virtual
~Person)) {};
virtual
void speak)){ cout « "Beer";}
class
Republican : virtual public Person
public:
Republican))
{}; virtual ~Republican)) {};
class
Quaker : virtual public Person
public:
Quaker))
{};
virtual
~Quaker() {};
virtual
void speak)) { cout « "Peace";}
class
Republican_Quaker : public Republican, public Quaker
public:
Republican_Quaker()
{};
virtual
"Republican_Quaker() {};
}
При создании
языка C++ преследовалась цель не усложнять механизм множественного наследования
по сравнению с единственным и разрешать все неоднозначности на стадии компиляции
[Stromtrup, 1977]. В этом существенное различие между C++ и SmallTalk.
В последнем такого рода конфликты разрешаются на стадии выполнения программы.
Это также отличается и от метода, основанного на списке предшествования классов,
который используется в CLOS. Кроме того, в языке CLOS конфликта имен, подобного
тому, который мы наблюдали с классом Person, быть просто не может, поскольку
все базовые классы с одинаковыми именами считаются идентичными.
Таким образом,
за высокую эффективность языка C++ приходится платить, тщательно продумывая
передачу свойств и поведения от классов родителей к производным классам с учетом
всех нюансов функционирования механизма наследственности в C++.
В этом отношении
C++ напоминает свой прототип — язык С, который требует гораздо более близкого
знакомства с работой компьютера, чем язык LISP, поскольку позволяет напрямую
обращаться к памяти компьютера, манипулировать адресами, формировать собственный
механизм выделения памяти и т.д. Какую стратегию предпочесть — зависит от индивидуальных
предпочтений разработчика, но если главным требованием к продукту является высокая
производительность, то чем большими возможностями управления ресурсами обладает
разработчик, тем лучше, тем более эффективную программу можно создать.
Суммируя все
сказанное о языке C++, отметим, что он вполне может послужить базовым программным
инструментом для создания экспертных систем. Если потребуется интерпретатор
порождающих правил, то можно либо разработать его самостоятельно (хотя это и
далеко не тривиальная задача), либо воспользоваться одним из имеющихся на рынке,
которые допускают внедрение в среду C++. Если вам удастся избежать описанных
выше сложностей в реализации множественного наследования, вы сможете в полной
мере воспользоваться многочисленными преимуществами этого языка — проверкой
статических типов, разделением между закрытым и общедоступным наследованием,
множеством средств защиты данных от случайных изменений.