Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Программирование на C / C++ / C++ for real programmers.pdf
Скачиваний:
261
Добавлен:
02.05.2014
Размер:
2.04 Mб
Скачать

173

};

// В файле grandpa.cpp

class RealGrandpa : public Grandpa {

// Промежуточный гомоморфный базовый класс protected:

// Функции двойной передачи

};

class Dad : public RealGrandpa { ... }; class AuntieEm : public RealGrandpa { ... };

Наличие производящих функций означает, что производные классы можно скрыть. Добавляя промежуточный базовый класс RealGrandpa, мы полностью прячем все жуткие подробности двойной передачи в файле .cpp. Никаких защищенных функций в файле .h!

Нет — конструкторам копий и оператору =!

Предполагается, что Grandpa — чисто гомоморфный базовый класс, содержащий хотя бы одну чисто виртуальную функцию. Это предотвращает непосредственное создание экземпляров Grandpa клиентом. Если вы используете производящую функцию для класса с возможностью создания экземпляров, конструкторы следует сделать защищенными, чтобы экземпляры могли создаваться только производящей функцией.

Раз уж мы заговорили на эту тему, после непродолжительных размышлений становится ясно, что клиент гомоморфного базового класса не должен использовать конструктор копий или оператор =. Если кто-нибудь захочет подублировать экземпляр, создайте специальную версию make-функции для копирования this.

class Grandpa { public:

virtual Grandpa* makeClone() = 0;

};

Эта функция не объявляется статической, поскольку в каждом производном классе она должна решать специализированную задачу. С присвоением дело обстоит сложнее. Если переопределить оператор = для левого операнда, непонятно, что же тогда делать с правым операндом, тип которого неизвестен. Первое практическое решение — полностью запретить присваивание в таких ситуациях и сделать оператор = закрытым. Второе — использовать вариацию на тему двойной передачи: сделать оператор = виртуальным и в каждом производном классе вызывать виртуальную функцию AssignTo(), перегружаемую для кажого производного класса. Смотрится уродливо, но работает.

Объекты классов

Объектом класса называется объект, предназначенный для создания экземпляров представляемого им типа. В нашей терминологии объект класса представляет собой объект, основные функции которого являются производящими. Позднее мы возложим на объекты классов и другие обязанности (например, описание структуры экземпляра), а пока рассмотрим практический пример.

class GrandpaClass {

// Объект класса для Grandpa

public:

 

Grandpa* make();

// Создает экземпляры Grandpa

};

 

class Grandpa { ... };

 

Все сказанное о производящих функциях относится и к объектам классов, включая спрятанные инкапсулированные производные классы, оптимизацию и прозрачность локализации. «Хорошо, — скажете вы, — но зачем это нужно?» Во-первых, мы избавляемся от некрасивых статических функций и переходим к более чистому, объектно-ориентированному варианту, при котором все происходит

174

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

Информация о классе

В объектах класса удобно хранить сведения о самом классе. Для этого лучше всего создать гомоморфный базовый класс для объектов классов.

class Class { protected:

Collection<Class> base_classes; Collection<Class> derived_classes; String class_name;

Class() {};

// Класс становится абстрактным базовым

public:

 

// Функции доступа для получения имени и т.д.

};

Состав хранимой информации в огромной степени зависит от потребностей приложения и от того, насколько увлеченно вы занимались SmallTalk на этой неделе.

Имя класса и создание экземпляров по имени

Базовая информация, которую может сообщить объект класса, — имя класса в виде некоторой символьной строки. Кроме того, можно хранить словарь всех объектов классов, индексируемый по имени класса. Если добавить к этому универсальный интерфейс к производящей функции, вам удастся реализовать возможность, которая не поддерживается в С++ напрямую — создание экземпляров по имени (instantiate by name).

// Где-то в клиентской программе

Class* c = gClasses.Find(“Grandpa”);

???* g = (Grandpa*)c->make(???);

Как видите, практическая польза такого подхода ограничивается некоторыми проблемами. Если мы уже знаем, что создается экземпляр Grandpa, то создание экземпляра по имени выглядит неразумно — нам будет трудно определить, к какому классу выполняется преобразование. Вдобавок данная схема не позволяет предоставить отдельные сигнатуры для производящих функций производных классов. Тем не менее, в некоторых ситуациях такая методика оказывается чрезвычайно полезной. Предположим, вы сохранили объект в виде потока байтов и теперь загружаете его. Если первые n байт содержит имя класса в виде символьной строки, вы сможете найти нужный объект класса для создания экземпляра. Как правило, реализация заканчивается созданием во всех классах Class функции make(istream&) или ее эквивалента. Программа приобретает следующий вид:

// В коде чтения потока cin << className;

Class* c = gClasses.Find(className); BasClass* obj = c->make(cin);

Иерархия классов

Сведения об иерархии классов можно хранить по разному, но в конечном счете все сводится к структурам данных с экземплярами Class. Выше был представлен один из вариантов: вести в каждом Class две коллекции, по одной для базовых и производных классов. Конечно, это следует понимать условно — речь идет об иерархии не объектов Class, а представленных ими классов. Необходимость различать понятия Class и «класс» наверняка вызовет у вас головную боль, но у поклонников SmallTalk и Lisp это считается хорошим развлечением и признаком мастерства. Другой способ — ввести одну глобальную структуру данных с парами (базовый, производный), индексируемую в обоих направлениях. В некоторых ситуациях вместо пар используются триплеты (базовый, производный, порядок), чтобы базовые классы перечислялись в порядке их объявления в соответствующем классе.

175

Описания переменных класса

В некоторых ситуациях объекты Class стоит сделать достаточно умными, чтобы они могли получать экземпляры представляемого класса и описывать их структуру. Все начинается с переменных класса. В наиболее прямолинейном варианте Class возвращает итератор, который в свою очередь возвращает пару смещение/Class, описывающую смещение переменной внутри экземпляра и ее Class. Имеет смысл создать перечисления для трех подмножеств переменных класса:

1. Все переменные, включая встроенные типы (такие как int). Для представления примитивных типов вам придется создать фиктивные классы, производные от Class.

2.Только переменные невстроенных типов, для которых обычно и так существует свой Class.

3.Только указатели и ссылки на другие объекты.

Последний вариант играет особенно важную роль в некоторых нетривиальных алгоритмах сборки мусора.

Описания функций класса

В еще более редких ситуациях объект Class должен описывать набор функций представленного им класса. При этом вы фактически начинаете играть роль компилятора С++, так что не перегибайте палку. И снова оптимальным представлением оказывается итератор. Для каждой функции можно возвращать любую информацию, от простейшей (ее имени) до более сложной (адрес, имена, типы и порядок аргументов и тип возвращаемого значения). Некоторые проблемы просто выходят за рамки С++; если вам захочется проделать нечто подобное, стоит серьезно подумать об использовании настоящего динамического языка.

Коллекции экземпляров

Класс Class предоставляет еще одну интересную возможность — ведение коллекции всех экземпляров класса. Это может быть либо отдельная коллекция для каждого Class, либо одна глобальная структура данных с парами (Class, экземпляр). Если выбран второй вариант и коллекция индексируется в обоих направлениях, она оказывается чрезвычайно полезной при отладке («Покажи мне все экземпляры класса x»), а также может применяться для ответов на вопросы вроде «Каков класс данного экземпляра?» без физического хранения адреса объекта Class в каждом экземпляре. Такое решение работает только для экземпляров верхнего уровня, создаваемых производящими функциями; вложенные объекты (переменные или базовые классы) в этот реестр не попадут.

Статистика

Показатели сыплются как из рога изобилия — количество экземпляров, в данный момент находящихся в памяти; общее количество экземпляров, созданных с момента запуска программы; статистические профили с описанием, когда и как создавались экземпляры… Возможности ограничены только тем, сколько времени вы сможете им посвятить. Если вы работаете на повременной оплате, не ограничивайте себя — ведь при желании обоснование можно придумать для любого показателя. А если нет, подумайте, окупятся ли потраченные усилия.

Еще несколько слов об уничтожающих функциях

После долгого разговора о том, какие замечательные штуки можно проделывать с объектами классов, вернемся к уничтожающим функциям. Многие концепции, представленные в предыдущем разделе (такие как скрытые коллекции экземпляров и статистика), реализуются лишь в том случае, если вам удастся отследить время создания и уничтожения экземпляра.

Конечно, для ведения статистики можно воспользоваться статистическими переменными, производящими функциями и т.д., принадлежащими целевому классу, однако методика, связанная с объектами классов, обеспечивает намного лучшую модульность. Возьмите существующий класс. Добавьте класс объекта Class. Влейте одну-две производящие функции, перемешайте с уничтожающей функцией. Поставьте на огонь статистики и доведите до кипения. Ура! Все административные средства были добавлены без модификации исходного класса. Об ихменениях придется сообщать клиентам, но если в начале работы никаких клиентов еще не было, а управляемый

176

класс должен оставаться неизменным или его исходные тексты недоступны, нам удалось довольно многого добиться, не создавая никаких побочных эффектов для критически важного кода.

Определение класса по объекту

Для существующего экземпляра довольно часто требуется определить его класс. Вроде бы ничего сложного, но в действительности это очень глубокая тема. Помните, что объекты могут создаваться в стеке или в куче, внедряться в другие объекты в виде переменных или базовых классов, а также создаваться производящими функциями. Ниже описано несколько основных решений.

Внедрение указателя на объект класса

Самое очевидное решение — внедрять указатель на Class в любой объект, вложенный или нет.

class Object {

// Предок всех реальных классов

protected:

 

 

static ObjectClass s_my_class;

Class* my_class;

// == &s_my_class;

public:

 

 

Object() : my_class(&s_my_class) {}

Class* My_Class()

{ return my_class; }

};

class Foo : public Object protected:

static FooClass s_my_class; public:

Foo() { my_class = &s_my_class; }

};

Все классы порождаются от общего предка Object, в котором определяется протокол для получения объекта Class. Вы имеете полное право использовать одни и те же имена членов на разных уровнях иерархии классов (как это сделано с s_my_class в нашем примере). Компилятор выбирает имя, находящееся в непосредственной области действия. Более того, конструкторы выполняются в порядке «базовый класс/переменные класса/ производные классы», поэтому последний конструктор оставит my_class правильное значение. Эта схема позволяет всегда получить объект Class независимо от того, сколько выполнялось преобразований типа от производных к базовым классам.

Издержки составляют четыре байта, необходимые для хранения указателя. Виртуальные функции не требуются, поэтому нам не придется добавлять v-таблицу в класс, обходившийся без нее. На избыточное конструирование my_class будут потрачены дополнительные такты процессора, но для большинства приложений это несущественно. Пожалуй, основные издержки сводятся к дополнительному коду, находящемуся в конструкторах.

В более «чистом» варианте указатель на Class задается производящими функциями объекта Class:

class Object { friend class Class; private:

Class* my_class; public:

Class* My_Class() { reutrn my_class; }

};

class Class { protected:

void SetClass(Object& obj) { obj.my_class = this; }

};

class Foo : public Object { ... };