straustrup2 (852740), страница 96
Текст из файла (страница 96)
Какправило, каркас образует дерево узловых классов. Прикладной программист поставляет только классы,служащие листьями в этом многоуровневом дереве, благодаря чему достигается общность междуразличными прикладными программами и упрощается повторное использование полезных функций,предоставляемых каркасом. Созданию каркаса могут способствовать библиотеки, в которыхопределяются некоторые полезные классы, например, такие как scrollbar ($$12.2.5) и dialog_box($$13.4).
После определения своих прикладных классов программист может использовать эти классы.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* в более удобноесемейство надежных классов, представляющих списки. Чтобы применение интерфейсных классов небыло слишком накладно, нужно использовать функции-подстановки.
В примерах, подобныхприведенному, где задача функций-подстановок только подогнать тип, накладные расходы в памяти искорости выполнения программы не возникают.Естественно, можно считать интерфейсным абстрактный базовый класс, который представляет355Бьерн Страуструп.Язык программирования С++абстрактный тип, реализуемый конкретными типами ($$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();};356Бьерн Страуструп.Язык программирования С++Отметим, что в действительности трудность возникла лишь потому, что у обеих функций 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] = ...}// ...}357Бьерн Страуструп.Язык программирования С++Очевидно, вариант с классом vec нагляднее и безопаснее.Интерфейсные классы имеют и другие важные области применения, например, интерфейс междупрограммами на С++ и программами на другом языке ($$12.1.4) или интерфейс с особымибиблиотеками С++.13.9 Управляющие классыКонцепция абстрактного класса дает эффективное средство для разделения интерфейса и егореализации.
Мы применяли эту концепцию и получали постоянную связь между интерфейсом,заданным абстрактным типом, и реализацией, представленной конкретным типом. Так, невозможнопереключить абстрактный итератор с одного класса-источника на другой, например, если исчерпаномножество (класс set), невозможно перейти на потоки.Далее, пока мы работаем с объектами абстрактного типа с помощью указателей или ссылок, теряютсявсе преимущества виртуальных функций. Программа пользователя начинает зависеть от конкретныхклассов реализации. Действительно, не зная размера объекта, даже при абстрактном типе нельзяразместить объект в стеке, передать как параметр по значению или разместить как статический. Еслиработа с объектами организована через указатели или ссылки, то задача распределения памятиперекладывается на пользователя ($$13.10).Существует и другое ограничение, связанное с использованием абстрактных типов.
Объект такогокласса всегда имеет определенный размер, но классы, отражающие реальное понятие, могут требоватьпамять разных размеров.Есть распространенный прием преодоления этих трудностей, а именно, разбить отдельный объект надве части: управляющую, которая определяет интерфейс объекта, и содержательную, в которойнаходятся все или большая часть атрибутов объекта. Связь между двумя частями реализуется спомощью указателя в управляющей части на содержательную часть. Обычно в управляющей частикроме указателя есть и другие данные, но их немного.