Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Cpp_Страуструп.doc
Скачиваний:
16
Добавлен:
03.05.2015
Размер:
3.2 Mб
Скачать

13.8 Интерфейсные классы

Про один из самых важных видов классов обычно забывают - это "скромные"

интерфейсные классы. Такой класс не выполняет какой-то большой

работы, ведь иначе, его не называли бы интерфейсным. Задача

интерфейсном класса приспособить некоторую полезную функцию к

определенному контексту. Достоинство интерфейсных классов в том,

что они позволяют совместно использовать полезную функцию, не загоняя

ее в жесткие рамки. Действительно, невозможно рассчитывать, что функция

сможет сама по себе одинаково хорошо удовлетворить самые разные запросы.

Интерфейсный класс в чистом виде даже не требует генерации кода.

Вспомним описание шаблона типа Splist из $$8.3.2:

template<class T>

class Splist : private Slist<void*> {

public:

void insert(T* p) { Slist<void*>::insert(p); }

void append(T* p) { Slist<void*>::append(p); }

T* get() { return (T*) Slist<void*>::get(); }

};

Класс Splist преобразует список ненадежных обобщенных указателей

типа void* в более удобное семейство надежных классов, представляющих

списки. Чтобы применение интерфейсных классов не было слишком накладно,

нужно использовать функции-подстановки. В примерах, подобных

приведенному, где задача функций-подстановок только подогнать

тип, накладные расходы в памяти и скорости выполнения программы

не возникают.

Естественно, можно считать интерфейсным абстрактный

базовый класс, который представляет абстрактный тип, реализуемый

конкретными типами ($$13.3), также как и управляющие классы

из раздела 13.9. Но здесь мы рассматриваем классы, у которых нет

иных назначений - только задача адаптации интерфейса.

Рассмотрим задачу слияния двух иерархий классов с помощью

множественного наследования. Как быть в случае коллизии

имен, т.е. ситуации, когда в двух классах используются виртуальные

функции с одним именем, производящие совершенно разные операции?

Пусть есть видеоигра под названием "Дикий запад", в которой диалог

с пользователем организуется с помощью окна общего вида (класс

Window):

class Window {

// ...

virtual void draw();

};

class Cowboy {

// ...

virtual void draw();

};

class CowboyWindow : public Cowboy, public Window {

// ...

};

В этой игре класс CowboyWindow представляет движение ковбоя на экране

и управляет взаимодействием игрока с ковбоем. Очевидно, появится

много полезных функций, определенных в классе Window и

Cowboy, поэтому предпочтительнее использовать множественное наследование,

чем описывать Window или Cowboy как члены. Хотелось бы передавать

этим функциям в качестве параметра объект типа CowboyWindow, не требуя

от программиста указания каких-то спецификаций объекта. Здесь

как раз и возникает вопрос, какую функции выбрать для CowboyWindow:

Cowboy::draw() или Window::draw().

В классе CowboyWindow может быть только одна функция с именем

draw(), но поскольку полезная функция работает с объектами Cowboy

или Window и ничего не знает о CowboyWindow, в классе CowboyWindow

должны подавляться (переопределяться) и функция Cowboy::draw(), и

функция Window_draw(). Подавлять обе функции с помощью одной -

draw() неправильно, поскольку, хотя используется одно имя, все же

все функции draw() различны и не могут переопределяться одной.

Наконец, желательно, чтобы в классе CowboyWindow наследуемые

функции Cowboy::draw() и Window::draw() имели различные однозначно

заданные имена.

Для решения этой задачи нужно ввести дополнительные классы для

Cowboy и Window. Вводится два новых имени

для функций draw() и гарантируется, что их вызов

в классах Cowboy и Window приведет к вызову функций с новыми именами:

class CCowboy : public Cowboy {

virtual int cow_draw(int) = 0;

void draw() { cow_draw(i); } // переопределение Cowboy::draw

};

class WWindow : public Window {

virtual int win_draw() = 0;

void draw() { win_draw(); } // переопределение Window::draw

};

Теперь с помощью интерфейсных классов CCowboy и WWindow можно

определить класс CowboyWindow и сделать требуемые переопределения

функций cow_draw() и win_draw:

class CowboyWindow : public CCowboy, public WWindow {

// ...

void cow_draw();

void win_draw();

};

Отметим, что в действительности трудность возникла лишь потому, что

у обеих функций draw() одинаковый тип параметров. Если бы типы

параметров различались, то обычные правила разрешения неоднозначности

при перегрузке гарантировали бы, что трудностей не возникнет, несмотря на

наличие различных функций с одним именем.

Для каждого случая использования интерфейсного класса можно

предложить такое расширение языка, чтобы требуемая адаптация

проходила более эффективно или задавалась более элегантным способом.

Но такие случаи являются достаточно редкими, и нет смысла чрезмерно

перегружать язык, предоставляя специальные средства для каждого

отдельного случая. В частности, случай коллизии имен при слиянии иерархий

классов довольно редки, особенно если сравнивать с

тем, насколько часто программист создает классы. Такие случаи

могут возникать при слиянии иерархий классов из разных

областей (как в нашем примере: игры и операционные системы).

Слияние таких разнородных структур классов всегда непростая задача,

и разрешение коллизии имен является в ней далеко не самой трудной

частью. Здесь возникают проблемы из-за разных стратегий обработки

ошибок, инициализации, управления памятью. Пример, связанный

с коллизией имен, был приведен потому, что предложенное решение:

введение интерфейсных классов с функциями-переходниками, - имеет

много других применений. Например, с их помощью можно менять

не только имена, но и типы параметров и возвращаемых значений,

вставлять определенные динамические проверки и т.д.

Функции-переходники CCowboy::draw() и WWindow_draw являются

виртуальными, и простая оптимизация с помощью подстановки невозможна.

Однако, есть возможность, что транслятор распознает такие функции

и удалит их из цепочки вызовов.

Интерфейсные функции служат для приспособления интерфейса к

запросам пользователя. Благодаря им в интерфейсе собираются операции,

разбросанные по всей программе. Обратимся к классу vector из $$1.4.

Для таких векторов, как и для массивов, индекс

отсчитывается от нуля. Если пользователь хочет работать с

диапазоном индексов, отличным от диапазона 0..size-1, нужно сделать

соответствующие приспособления, например, такие:

void f()

{

vector v(10); // диапазон [0:9]

// как будто v в диапазоне [1:10]:

for (int i = 1; i<=10; i++) {

v[i-1] = ... // не забыть пересчитать индекс

}

// ...

}

Лучшее решение дает класс vec c произвольными границами индекса:

class vec : public vector {

int lb;

public:

vec(int low, int high)

: vector(high-low+1) { lb=low; }

int& operator[](int i)

{ return vector::operator[](i-lb); }

int low() { return lb; }

int high() { return lb+size() - 1; }

};

Класс vec можно использовать без дополнительных операций, необходимых

в первом примере:

void g()

{

vec v(1,10); // диапазон [1:10]

for (int i = 1; i<=10; i++) {

v[i] = ...

}

// ...

}

Очевидно, вариант с классом vec нагляднее и безопаснее.

Интерфейсные классы имеют и другие важные области применения,

например, интерфейс между программами на С++ и программами на другом

языке ($$12.1.4) или интерфейс с особыми библиотеками С++.