Бьерн Страуструп.
Язык программирования С++
356
абстрактный тип, реализуемый конкретными типами ($$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();
};
Бьерн Страуструп.
Язык программирования С++
357
Отметим, что в действительности трудность возникла лишь потому, что у
обеих функций 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]
=
...
}
//
...
}
Бьерн Страуструп.
Язык программирования С++
358
Очевидно, вариант с классом vec нагляднее и безопаснее.
Интерфейсные классы имеют и другие важные области применения, например, интерфейс между
программами на С++ и программами на другом языке ($$12.1.4) или интерфейс с
особыми
библиотеками С++.
Достарыңызбен бөлісу: