Бьерн Страуструп - Язык программирования С++. Главы 11-13
Страница 56. Управляющие классы



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())
           {
              // ...
           }
           // ...
         }

         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*) = 0;
           virtual void remove(T*) = 0;

           virtual int is_member(T*) = 0;

           virtual T* first() = 0;
           virtual T* next() = 0;

           set() : handle_count(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.
    На практике иногда приходится извлекать указатель на содержательную
часть из управляющего класса и пользоваться непосредственно им.
Можно, например, передать такой указатель функции, которая ничего
не знает об управляющем классе. Если функция не уничтожает объект,
на который она получила указатель, и если она не сохраняет указатель
для дальнейшего использования после возврата, никаких ошибок быть
не должно. Может оказаться полезным переключение управляющего класса
на другую содержательную часть:

          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(); }
             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 следует блокировать. Как
показывает опыт автора, типичный случай, когда операции до и после
обращения к объекту надо выполнять не для всех, а только для некоторых
функций-членов. Блокировка всех операций, как это делается в
мониторах некоторых операционных систем, является избыточной и может
существенно ухудшить параллельный режим выполнения.
   Переопределив все функции интерфейса в управляющем классе, мы
получили по сравнению с приемом перегрузки операции ->, то
преимущество, что теперь можно строить производные
от set_controller классы. К сожалению, мы можем потерять и некоторые
достоинства управляющего класса, если к производным классам будут
добавляться члены, представляющие данные. Можно сказать, что
программный объем, который разделяется между управляемыми классами
уменьшается по мере роста программного объема управляющего класса.

 
« Предыдущая статья   Следующая статья »