Опубликован: 10.10.2006 | Уровень: специалист | Доступ: платный
Лекция 13:

Проектирование библиотек

< Лекция 12 || Лекция 13: 1234567891011

13.9 Управляющие классы

Концепция абстрактного класса дает эффективное средство для разделения интерфейса и его реализации. Мы применяли эту концепцию и получали постоянную связь между интерфейсом, заданным абстрактным типом, и реализацией, представленной конкретным типом. Так, невозможно переключить абстрактный итератор с одного класса-источника на другой, например, если исчерпано множество (класс set ), невозможно перейти на потоки.

Далее, пока мы работаем с объектами абстрактного типа с помощью указателей или ссылок, теряются все преимущества виртуальных функций. Программа пользователя начинает зависеть от конкретных классов реализации. Действительно, не зная размера объекта, даже при абстрактном типе нельзя разместить объект в стеке, передать как параметр по значению или разместить как статический. Если работа с объектами организована через указатели или ссылки, то задача распределения памяти перекладывается на пользователя ( \S 13.10).

Существует и другое ограничение, связанное с использованием абстрактных типов. Объект такого класса всегда имеет определенный размер, но классы, отражающие реальное понятие, могут требовать память разных размеров.

Есть распространенный прием преодоления этих трудностей, а именно, разбить отдельный объект на две части: управляющую, которая определяет интерфейс объекта, и содержательную, в которой находятся все или большая часть атрибутов объекта. Связь между двумя частями реализуется с помощью указателя в управляющей части на содержательную часть. Обычно в управляющей части кроме указателя есть и другие данные, но их немного. Суть в том, что состав управляющей части не меняется при изменении содержательной части, и она настолько мала, что можно свободно работать с самими объектами, а не с указателями или ссылками на них.

управляющая часть           содержательная часть

Простым примером управляющего класса может служить класс string из \S 7.6. В нем содержится интерфейс, контроль доступа и управление памятью для содержательной части. В этом примере управляющая и содержательная части представлены конкретными типами, но чаще содержательная часть представляется абстрактным классом.

Теперь вернемся к абстрактному типу set из \S 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; }
   // ...
};

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

< Лекция 12 || Лекция 13: 1234567891011
Дарья Федотова
Дарья Федотова
Сергей Березовский
Сергей Березовский

В рамках проф. переподготовки по программе "Программирование"

Есть курсы, которые я уже прошел. Но войдя в курс я вижу, что они не зачтены (Язык Ассемблера и архитектура ЭВМ, Программирование на С++ для профессионалов). Это как?

алексей оглы
алексей оглы
Россия
рафич Салахиев
рафич Салахиев
Россия