Б. Страуструп - Язык программирования С++ (1119446), страница 100
Текст из файла (страница 100)
Какбыть в случае коллизии имен, т.е. ситуации, когда в двух классах используются виртуальные функции содним именем, производящие совершенно разные операции? Пусть есть видеоигра под названием"Дикий запад", в которой диалог с пользователем организуется с помощью окна общего вида (класс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).Существует и другое ограничение, связанное с использованием абстрактных типов.
Объект такогокласса всегда имеет определенный размер, но классы, отражающие реальное понятие, могут требоватьпамять разных размеров.Есть распространенный прием преодоления этих трудностей, а именно, разбить отдельный объект надве части: управляющую, которая определяет интерфейс объекта, и содержательную, в которойнаходятся все или большая часть атрибутов объекта. Связь между двумя частями реализуется спомощью указателя в управляющей части на содержательную часть. Обычно в управляющей частикроме указателя есть и другие данные, но их немного.
Суть в том, что состав управляющей части неменяется при изменении содержательной части, и она настолько мала, что можно свободно работать ссамими объектами, а не с указателями или ссылками на них.управляющая частьсодержательная частьПростым примером управляющего класса может служить класс string из $$7.6. В нем содержитсяинтерфейс, контроль доступа и управление памятью для содержательной части. В этом примереуправляющая и содержательная части представлены конкретными типами, но чаще содержательнаячасть представляется абстрактным классом.Теперь вернемся к абстрактному типу set из $$13.3.
Как можно определить управляющий класс дляэтого типа, и какие это даст плюсы и минусы? Для данного класса set можно определить управляющийкласс просто перегрузкой операции ->:class set_handle {set* rep;public:set* operator->() { return rep; }set_handler(set* pp) : rep(pp) { }};Это не слишком влияет на работу с множествами, просто передаются объекты типа set_handle вместообъектов типа set& или set*, например:void my(set_handle s){for (T* p = s->first(); p; p = s->next()){// ...}// ...}358Бьерн Страуструп.Язык программирования С++void your(set_handle s){for (T* p = s->first(); p; p = s->next()){// ...}// ...}void user(){set_handle sl(new slist_set);set_handle v(new vector_set v(100));my(sl);your(v);my(v);your(sl);}Если классы set и set_handle разрабатывались совместно,легко реализовать подсчет числасоздаваемых множеств:class set {friend class set_handle;protected:int handle_count;public:virtual void insert(T*) =virtual void remove(T*) =virtual int is_member(T*)virtual T* first() = 0;virtual T* next() = 0;set() : handle_count(0) {};0;0;= 0;}Чтобы подсчитать число объектов данного типа set, в управляющем классе нужно увеличивать илиуменьшать значение счетчика set_handle:class set_handle {set* rep;public:set* operator->() { return rep; }set_handle(set* pp): rep(pp) { pp->handle_count++; }set_handle(const set_handle& r): rep(r.rep) { rep->handle_count++; }set_handle& operator=(const set_handle& r){rep->handle_count++;if (--rep->handle_count == 0) delete rep;rep = r.rep;return *this;}~set_handle(){ if (--rep->handle_count == 0) delete rep; }};Если все обращения к классу set обязательно идут через set_handle, пользователь может небеспокоиться о распределении памяти под объекты типа set.На практике иногда приходится извлекать указатель на содержательную часть из управляющего классаи пользоваться непосредственно им.
Можно, например, передать такой указатель функции, которая359Бьерн Страуструп.Язык программирования С++ничего не знает об управляющем классе. Если функция не уничтожает объект, на который она получилауказатель, и если она не сохраняет указатель для дальнейшего использования после возврата, никакихошибок быть не должно. Может оказаться полезным переключение управляющего класса на другуюсодержательную часть:class set_handle {set* rep;public:// ...set* get_rep() { return rep; }void bind(set* pp){pp->handle_count++;if (--rep->handle_count == 0) delete rep;rep = pp;}};Создание новых производных от set_handle классов обычно не имеет особого смысла, поскольку это конкретный тип без виртуальных функций.
Другое дело - построить управляющий класс для семействаклассов, определяемых одним базовым. Полезным приемом будет создание производных от такогоуправляющего класса. Этот прием можно применять как для узловых классов, так и для абстрактныхтипов.Естественно задавать управляющий класс как шаблон типа:template<class T> class handle {T* rep;public:T* operator->() { return rep; }// ...};Но при таком подходе требуется взаимодействие между управляющим и "управляемым" классами.Если управляющий и управляемые классы разрабатываются совместно, например, в процессесоздания библиотеки, то это может быть допустимо. Однако, существуют и другие решения ($$13.10).За счет перегрузки операции -> управляющий класс получает возможность контроля и выполнениякаких-то операций при каждом обращении к объекту. Например, можно вести подсчет частотыиспользования объектов через управляющий класс:template<class T>class Xhandle {T* rep;int count;public:T* operator->() { count++; return rep; }// ...};Нужна более сложная техника, если требуется выполнять операции как перед, так и после обращения кобъекту.
Например, может потребоваться множество с блокировкой при выполнении операцийдобавления к множеству и удаления из него. Здесь, по сути, в управляющем классе приходитсядублировать интерфейс с объектами содержательной части:class set_controller {set* rep;// ...public:lock();unlock();virtual void insert(T* p){ lock(); rep->insert(p); unlock(); }360Бьерн Страуструп.Язык программирования С++virtual void remove(T* p){ lock(); rep->remove(p); unlock(); }virtual int is_member(T* p){ return rep->is_member(p); }virtual T* first() { return rep->first(); }virtual T* next() { return rep->next(); }// ...};Писать функции-переходники для всего интерфейса утомительно (а значит могут появляться ошибки),но не трудно и это не ухудшает характеристик программы.Заметим, что не все функции из set следует блокировать. Как показывает опыт автора, типичныйслучай, когда операции до и после обращения к объекту надо выполнять не для всех, а только длянекоторых функций-членов.