Бьерн Страуструп. Язык программирования С++



бет122/124
Дата16.07.2016
өлшемі3.27 Mb.
#204081
түріКнига
1   ...   116   117   118   119   120   121   122   123   124

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 handle {

T* rep;


public:

T* operator->() { return rep; }

// ...

};
Но при таком подходе требуется взаимодействие между



управляющим и "управляемым" классами. Если управляющий и управляемые

классы разрабатываются совместно, например, в процессе создания

библиотеки, то это может быть допустимо. Однако, существуют и другие

решения ($$13.10).

За счет перегрузки операции -> управляющий класс получает

возможность контроля и выполнения каких-то операций при каждом

обращении к объекту. Например, можно вести подсчет частоты

использования объектов через управляющий класс:


template

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 классы. К сожалению, мы можем потерять и некоторые

достоинства управляющего класса, если к производным классам будут

добавляться члены, представляющие данные. Можно сказать, что

программный объем, который разделяется между управляемыми классами

уменьшается по мере роста программного объема управляющего класса.






Достарыңызбен бөлісу:
1   ...   116   117   118   119   120   121   122   123   124




©dereksiz.org 2024
әкімшілігінің қараңыз

    Басты бет