Программирование на Visual C++. Архив рассылки - Алекс Jenter
Шрифт:
Интервал:
Закладка:
В MFC эта функция-член называется Construct(). Она создается макросами IMPLEMENT_DYNCREATE или IMPLEMENT_SERIAL. Один из этих макросов обязательно должен появиться в .cpp-модуле ровно один раз для каждого класса с поддержкой динамического создания. В случае со Scribble, выражение IMPLEMENT_DYNCREATE(CScribDoc, CDocument) появляется почти в самом начале файла SCRIBDOC.CPP. Первым аргументом идет класс, а вторым – его класс-родитель. Листинг 2 показывает код, сгенерированный препроцессором.
Когда MFC нужен документ или любой другой объект класса, унаследованного от CObject, она вызывает функцию CreateObject() класса CRuntimeClass. CreateObject() выделяет память, используя размер, указанный в структуре CRuntimeClass, и после этого вызывает ConstructObject(). ConstructObject() проверяет, поддерживает ли класс динамическое конструирование и вызывает функцию Construct() создаваемого класса.
Хотя в исходных текстах не дается пояснений, ясно, что такая схема четко разделяет конструирование объекта и выделение памяти. Все это кажется лишним, но в определенных ситуациях без такой организации не обойтись. Например, чтобы при создании массива его элементы располагались в одном блоке памяти, эту память нужно выделять одним вызовом функции malloc(). Используя ConstructObject(), Вы можете вручную инициализировать каждый элемент. Такой механизм позволяет принимать на этапе выполнения решения, которые в C++ обычно принимаются на этапе компиляции.
В примере 2 показана функция Construct(). Синтаксис вызова new немного необычен. На самом деле вызывается функция CObject::operator new(size_t, void*). Помните, что размер структуры — это подразумеваемый аргумент при вызове new, однако его следует явно описать в определении оператора. Эта версия new в CObject ничего не делает, но вызов new дает побочным эффектом вызов конструктора для этого объекта. Память уже была выделена вызовом CreateObject с использованием информации о размере из CRuntimeClass.
Пример 2: Функция Construct().
void __stdcall CScribDoc::Construct(void* p) {
new(p) CScribDoc;
}
Используя реестр классов CRuntimeClass и функцию-член Construct(), MFC удается находить и создавать объекты новых типов на лету, что решает Проблему 1. Потенциально серьезные последствия данной техники в том, что при этом не поддерживаются множественное наследование и виртуальные базовые классы (см. MFC Technical Note #16).
Регистрация типовПроблема 2 состоит в том, что пользователи должны иметь возможность легко добавлять новые классы в реестр. Идея саморегистрирующихся типов – ключевая идея объектно-ориентированного проектирования. Если каждый тип сам регистрирует факт своего существования в реестре, вместо того, чтобы программист прописывал его в реестре заранее, тогда типы можно свободно добавлять и удалять из программы, не меняя структуры реестра.
Хоть это и неочевидно, именно макрос IMPLEMENT_DYNCREATE позволяет пользователям без проблем добавлять новые классы в реестр. После развертывания IMPLEMENT_DYNCREATE, как показано на листинге 2, статическая структура CRuntimeClass в CScribDoc инициализируется так, как показано в примере 3.
Листинг 2
void__stdcall CScribDoc::Construct(void* p) {
new(p) CScribDoc;
}
CRuntimeClass* __stdcall CScribDoc::_GetBaseClass() {
return (&CDocument::classCDocument);
}
CRuntimeClass CScribDoc::classCScribDoc = {
"CScribDoc", sizeof(CScribDoc), 0xFFFF, CScribDoc::Construct,
&CScribDoc::_GetBaseClass, 0 };
static const AFX_CLASSINIT _init_CScribDoc(&CScribDoc::classCScribDoc);
CRuntimeClass* CScribDoc::GetRuntimeClass() const {
return &CScribDoc::classCScribDoc;
}
Пример 3: Инициализация статической структуры CRuntimeClass в CScribDoc.
CRuntimeClass CScribDoc::classCScribDoc = {
"CScribDoc", sizeof(CScribDoc), 0xFFFF, CScribDoc::Construct, &CScribDoc::GetBaseClass, 0
};
Некоторые из элементов этой структуры мы уже рассматривали. В частности, выражение sizeof(CScribDoc) используется CreateObject для выделения нужного объема памяти; затем эта память инициализируется функцией, на которую указывает CScribDoc::Construct.
Такой механизм делает возможной регистрацию типов объектов на лету каждый раз, когда они линкуются с программой, решая, таким образом, Проблему 2.
Часто разработчики задаются вопросом – в чем разница между различными макросами DECLARE и IMPLEMENT? Все макросы DECLARE_DYNAMIC и IMPLEMENT_DYNAMIC определяют статическую структуру CRuntimeClass, подобно DYNCREATE, описанному ранее, за одним исключением – поле Construct в этой структуре установлено в NULL. DECLARE_DYNCREATE и IMPLEMENT_DYNCREATE передают в структуру адрес функции Construct() для динамического создания типа. DECLARE_SERIAL и IMPLEMENT_SERIAL основываются на макросах DYNCREATE, но заменяют поле со значением 0xFFFF на номер схемы этой структуры.
Макросы SERIAL также определяют для класса operator>>. Этот оператор требует особого подхода, так как ему передается указатель на класс, но ни один из экземпляров этого класса не будет существовать, пока экземпляр не будет загружен из файла. Без экземпляра класса, MFC не может получить доступ к информации о классе времени выполнения для проверки на то, что загружаемый объект является объектом того же класса (или класса-наследника), что и переданный указатель. Перегружая operator>>, MFC получает возможность передавать указатель на информацию о типе времени выпонения, чтобы механизм сериализации не зависел от типа (typesafe serialization).
Создание типов из файлаТретья проблема состоит в том, чтобы создать механизм сопоставления для создания типов по информации, прочитанной из файла. Учитывая то, что компилятор требует уникальности имен классов и то, что имя класса уже включено в структуру CRuntimeClass, имя класса является идеальным кандидатом на запись в файл и последующую идентификацию класса.
Итак, при сохранении объекта в архиве можно записать туда имя класса и его данные. MFC так и делает, плюс проводит дополнительную работу для каждого сериализуемого класса. Имя класса берется из структуры CRuntimeClass, которая возвращается виртуальной функцией объекта. Определение типа производится динамически во время выполнения, поэтому структура типа Tiger будет корректно записана даже в случае, если MFC передается указатель на ее базовый класс типа Animal. Эта типонезависимость очень важна. Любая функция может без опасений сохранить объект в архиве, даже если точный тип объекта неизвестен.
То же касается и восстановления объекта из архива. Возвращаясь к примеру в начале статьи, несколько простых выражений из примера 4 заставляют MFC успешно загружать корректный тип документа из файла.
Пример 4: Загрузка правильного типа документа из файла.
CDocument* pDoc;
CArchive& ar;
…
ar >> pDoc;
В реализации operator>> MFC загружает из файла имя класса, и ищет это имя в списке типов. Если этот тип присутствует в реестре и был описан либо как DECLARE_DYNCREATE, либо как DECLARE_SERIAL, MFC может сконструировать требуемый объект. Непосредственная загрузка данных этого объекта возлагается на сам объект вызовом его виртуальной функции Serialize() , что решает Проблему 3.
Тип создаваемого объекта не привязан к типу запрошенного объекта. Если класс-потомок загружается через указатель на класс-предок, как в примере с CDocument, все равно создастся корректный класс-потомок. Это единственный способ корректно задать указатель vtbl так, чтобы он указывал на виртуальные функции объекта. Если объект в архиве не является "родственником" запрошенного объекта, MFC взведет исключение.
Этот механизм таит в себе две потенциальные ловушки для разработчиков. Во-первых, переименование сериализуемой структуры или класса сделает невозможным его восстановление из старых архивов. Во-вторых, MFC не записывает в архив длину каждого объекта. Поэтому, если MFC не сможет загрузить объект, она не сможет пропустить его и загрузить оставшуюся часть архива.
Оптимизация архивовКогда я впервые просматривал этот код, мне виделись распухшие файлы с объектами и большие задержки при постоянном просмотре связанного списка. Но архивы MFC остаются маленькими, а скорость выполнения высокой благодаря хэш-идентификаторам.
MFC ведет хэш-таблицу всех классов и объектов, которые записываются в архив. При повторной записи объекта, MFC записывает вместо него идентификатор. Таким образом, при восстановлении информации из архива связанный список типов проходится только для новых классов. При загрузке объекта, экземпляры класса которого уже были прочитаны, нужная структура CRuntimeClass находится поиском в хэш-таблице.