|
Про один из самых важных видов классов обычно забывают - это "скромные"
интерфейсные классы. Такой класс не выполняет какой-то большой
работы, ведь иначе, его не называли бы интерфейсным. Задача
интерфейсном класса приспособить некоторую полезную функцию к
определенному контексту. Достоинство интерфейсных классов в том,
что они позволяют совместно использовать полезную функцию, не загоняя
ее в жесткие рамки. Действительно, невозможно рассчитывать, что функция
сможет сама по себе одинаково хорошо удовлетворить самые разные запросы.
Интерфейсный класс в чистом виде даже не требует генерации кода.
Вспомним описание шаблона типа 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) или интерфейс с особыми библиотеками С++.