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++. Если вам удастся избежать описанных выше сложностей в реализации множественного наследования, вы сможете в полной мере воспользоваться многочисленными преимуществами этого языка — проверкой статических типов, разделением между закрытым и общедоступным наследованием, множеством средств защиты данных от случайных изменений.