Модераторы: Daevaorn
  

Поиск:

Ответ в темуСоздание новой темы Создание опроса
> DECLARE_DYNCREATE() 
:(
    Опции темы
chipset
Дата 28.3.2005, 04:22 (ссылка) |    (голосов:1) Загрузка ... Загрузка ... Быстрая цитата Цитата


Эксперт
****


Профиль
Группа: Экс. модератор
Сообщений: 4067
Регистрация: 11.1.2003
Где: Seattle, US

Репутация: 27
Всего: 164



 Идея такова:
- если существует необходимость во время работы программы определить, какого класса объект, имея в наличии только указатель на него, или
- если нужно создать объект такого же класса, не зная, какого именно класса, но имея в наличии указатель на "готовый" объект, или
- если нужно загрузить из файла/потока массив объектов различных классов, не зная заранее, в какой последовательности они записаны, или
- решать похожие задачи
то
нужно хранить внутри каждого объекта в каком-то виде информацию о классе, которому принадлежит объект (а также желательно там-же хранить информацию о базовом классе(-ах), и указатель на функцию-конструктор объектов данного класса). Для экономии памяти (чтобы не хранить внутри ста объектов одного и того же класса одинаковую информацию), можно использовать статические компонентные данные. Чтобы как-то "объединить" имя класса, указатель на функцию-конструктор, информацию о базовом классе, можно сделать специальный класс-хранитель инфы о классе (как CRuntimeClass в MFC).

Для подобных целей Borland ввела технологию RTTI (run-time type identification). Со своей стороны, Microsoft начиная с четвертой версии MFC, отказалась от RTTI и ввела собственную технологию (связка "CObject+CRuntimeClass+макросы 3-х видов").

Итак, чем же отличается реализация Microsoft:
- информация о классе, перечисленная выше, хранится в объекте CRuntimeClass. Для сравнения классов двух объектов, достаточно сравнить указатели на CRuntimeClass-ы, которые можно получить используя GetRuntimeClass либо макрос RUNTIME_CLASS:
Код
ClassA  A;
ClassB  B;
if(A.GetRuntimeClass() == B.GetRuntimeClass()) { /* какие-то действия */ }
if(RUNTIME_CLASS(ClassA) == RUNTIME_CLASS(ClassB)) { /* ... */ }

- для создания объекта любого класса, используется метод CRuntimeClass::CreateObject. Например, создать объект того же класса, что и указанный объект, можно так:
Код
void  *pUnknownObject;  // указатель на объект любого класса
CRuntimeClass *pRTC = ((CObject *)pUnknownObject)->GetRuntimeClass();
CObject  *pNewObject = pRTC->CreateObject();

- для получения имени класса:
Код
CRuntimeClass *pRTC = pUnknownObject->GetRuntimeClass();
CString csClassName = pRTC->m_lpszClassName;

- для определения, является ли класс A производным от класса B:
Код
CRuntimeClass  *pRTC_A = RUNTIME_CLASS(ClassA);
CRuntimeClass  *pRTC_B = RUNTIME_CLASS(ClassB);
// или
CRuntimeClass  *pRTC_A = pUnknownObjectA->GetRuntimeClass();
CRuntimeClass  *pRTC_B = pUnknownObjectA->GetRuntimeClass();

if(pRTC_A->IsDerivedFrom(pRTC_B)) { /* ... */ }

- для загрузки из архива (аналог потока в MFC) объекта неизвестного заранее класса:
Код
CFile file("c:\\1.dat", CFile::modeRead);
CArchive  ar(&file, CArchive::load);

CObject  *pLoadingObject = NULL;
ar>>pLoadingObject;


И, наконец, требования к классам пользователя, поддерживающим такую технологию:
- все объекты, поддерживающие такую технологию д.б. производны от CObject
- в определение класса нужно включить один из макросов:
DECLARE_DYNAMIC - если нужно только иметь в объекте CRuntimeClass;
DECLARE_DYNCREATE - если хочется еще и создавать объекты с пом. CreateObject;
DECLARE_SERIAL - если хочется еще и грузить объекты из архива, не зная класса.
Все три макроса имеют только 1 аргумент - имя класса.
- вне класса нужно добавить макрос IMPLEMENT_xxx,   где "xxx" - соответственно "DYNAMIC", "DYNCREATE" или "SERIAL". Две первые версии макросов идут с 2-мя параметрами - имя класса и базового класса; третья имеет еще один параметр - номер версии объекта (нужно для поддержки "супер-пупер" работы с версиями объектов smile, я никогда не сталкивался с необходимостью использования этой фичи, версию везде ставлю в 0).

Пример:
Код
// SomeClass.h

class CSomeClass: public CObject
{
public:
      //  здесь м.б. что угодно
      
      DECLARE_SERIAL(CSomeClass);
};

// SomeClass.cpp

IMPLEMENT_SERIAL(CSomeClass, CObject, 0)


Недостатки мелкософтовской системы:
- у класса м.б. только 1 базовый класс (точнее, из может быть сколько угодно, но с пом. IsDerivedFrom можно определить только, производен ли класс от того, что был прописан в IMPLEMENT_xxx)
- всё время приходится писать эти проклятые макросы smile

В новой версии MFC (7.0) добавлено пару мелких фич, но система в целом та же. Если честно, некоторые вещи меня сильно не устраивают во всей этой системе (например, если загружена DLL со своими классами, они "видны" только внутри этой DLL; создать объект по имени класса а не по CRuntimeClass можно только в MFC 7.0, и др.), но идея ИМХО в целом неплоха, работоспособна и м.б. использована там где это надо.

Продолжая тему, хотелось бы узнать, что скрывается под всеми этими макросами, вставляемыми в каждый класс ? А на самом деле всё достаточно просто и логично. В зависимости от того, как подключается MFC, содержимое каждого макроса может несколько видоизменяться. Далее будут приводиться только "обобщенные" версии макросов, без модификаторов "const", "AFX_DATA"  и без "характерных" методов для каждого типа подключения MFC.

Итак, первый макрос:
Код
// DECLARE_DYNAMIC(class_name):
public:
      static AFX_DATA CRuntimeClass class##class_name;
      virtual CRuntimeClass* GetRuntimeClass() const;

Как видим, в каждый класс, содержащий макрос DECLARE_DYNCREATE, включается статический компонент "CRuntimeClass class##class_name", где "##" означают подстановку значения аргумента макроса. Например, если мы пишем:
Код
class MyClassA
{
      DECLARE_DYNCREATE(MyClassA)
};

то получим:
Код
class MyClassA
{
public:
      static AFX_DATA CRuntimeClass classMyClassA;
      virtual CRuntimeClass* GetRuntimeClass() const;
};

Как известно, статические компонентные данные классов нужно инициализировать. Что это означает - вне класса нужно дать полное имя компонентного данного с инициализирующим значением. Именно этим (а также реализацией метода GetRuntimeClass) и "занимается" макрос IMPLEMENT_DYNAMIC (используя другой макрос - IMPLEMENT_RUNTIMECLASS):
Код
// IMPLEMENT_RUNTIMECLASS(class_name, base_class_name, wSchema, pfnNew)
      CRuntimeClass class_name::class##class_name = {
      #class_name, sizeof(class class_name), wSchema, pfnNew,
      RUNTIME_CLASS(base_class_name), NULL };
      
      CRuntimeClass* class_name::GetRuntimeClass() const
      { return RUNTIME_CLASS(class_name); }

// IMPLEMENT_DYNAMIC
IMPLEMENT_RUNTIMECLASS(class_name, base_class_name, 0xFFFF, NULL)

Отдельного упоминания здесь заслуживает, разве что, "wShema", относящаяся к "супер-пупер" поддержке версий объектов (advanced versioning support), абсолютно бесполезная, имхо, фича. Остальное ясно: макрос IMPLEMENT_DYNAMIC вызывает вложенный макрос IMPLEMENT_RUNTIMECLASS, который инициализирует статический компонент "CRuntimeClass class##class_name", записывая в поля нашего объекта "classMyClassA" такие данные, как имя класса, адрес конструктора (он равен нулю, т.к. для поддержки динамического создания объектов по CRuntimeClass'у, нужно использовать DYNCREATE-макросы), и указатель на CRuntimeClass базового класса. ЗЫ, при динамическом подключении MFC вместо указателя на базовый CRuntimeClass используется указатель на функцию "_GetBaseClass", но от этого принципиально ничего не изменяется.

Далее, перейдем к оставшимся двум видам макросов (мы рассмотрели "DYNAMIC") - "DYNCREATE" и "SERIAL", а также поподробнее рассмотрим, где же MFC хранит список доступных классов (да-да, оказывается есть такой "скрытый" список, хе-хе smile) и как работает механизм сериализации (сохранение и восстановление объекта из файла/потока) с точки зрения CRuntimeClass'ов.

DYNCREATE-макросы добавляют возможность не только определить во время программы, какому классу принадлежит объект (эту возможность даёт рассмотренный DYNAMIC-макрос), но и создать объект определённого класса, используя специальную функцию "CreateObject", входящую в состав как CRuntimeClass'а, так и нашего "MyClassA".

Как это реализуется:
Код
// DECLARE_DYNCREATE(class_name)
    DECLARE_DYNAMIC(class_name)
    static CObject* PASCAL CreateObject();

Здесь всё понятно: во-первых, подключается рассмотренный макрос DECLARE_DYNAMIC, а во-вторых, в наш класс добавляется метод CreateObject.
Код
// IMPLEMENT_DYNCREATE(class_name, base_class_name)
    CObject* PASCAL class_name::CreateObject()
    { return new class_name; }

    IMPLEMENT_RUNTIMECLASS(class_name, base_class_name, 0xFFFF,
            class_name::CreateObject)

Тут реализуется метод "CreateObject", и указатель на этот метод передаётся во вложенный макрос IMPLEMENT_RUNTIMECLASS (инициализирующий CRuntimeClass), чтобы CRuntimeClass мог создавать объекты именно нашего класса (имя которому class_name=="MyClassA"), используя указатель на CreateObject.

Для полноты картины остаётся рассмотреть сериализацию (SERIAL-макросы) и глобальный список классов.

Ну и что это за зверь такой - сериализация ? - спросите вы.
В переводе это означает "преобразование в последовательную форму". Значит ясно, что это технология, позволяющая записывать и восстанавливать данные в/из последовательной формы (файла, потока). Для записи и чтения объектов, данных и т.п. в MFC используется довольно-таки удобный класс CArchive. У него определены операторы "<<" и ">>", смысл которых, я думаю, ясен  (особенно тем, кто много работает с STL), эти операторы поддерживают все простые типы данных, плюс строки (CString), плюс - внимание - типы, определяемые пользователем. Ну да, скажете вы, как же CArchive может догадаться, как именно сохранять мой супер-пупер-навороченный объект (хранящий, помимо всего, сотню вложенных объектов разных классов и десяток контейнеров STL smile) ? 

Да очень просто. В нашем классе "MyClassA" необходимо переопределить виртуальную функцию "Serialize", выполняющую всю работу по сохранению и загрузке внутренних данных класса "MyClassA". К примеру, если наш класс содержит только переменную типа "int", то "Serialize" будет выглядеть так:
Код
class MyClassA: public CObject
{
public:
      int i;
      virtual void Serialize(CArchive &ar)
      {
            if(ar.IsLoading())
                  ar>>i;
            else
                  ar<<i;
      }

      ...


Откуда же будет вызвана функция "Serialize" ? Из операторов ">>" и "<<". Никаких проблем с тем, что в классе CArchive не определены такие операторы для нашего класса "MyClassA", достаточно взглянуть на исходник:
Код
class  SomeClass
{
     ...
}

...

CArchive& operator>>(CArchive& ar, SomeClass &Object)

    ...
}


Именно для поддержки оператора ">>" предназначены SERIAL-макросы,  (оператор "<<" можно сделать одинаковым для всех, т.к. в отличие от ">>", на момент записи в архив известен класс объекта).

Содержимое SERIAL-макросов:
Код
// define DECLARE_SERIAL(class_name)
     DECLARE_DYNCREATE(class_name)
     friend CArchive& operator>>(CArchive& ar, class_name* &pOb);


// IMPLEMENT_SERIAL(class_name, base_class_name, wSchema)
     CObject* PASCAL class_name::CreateObject()
     { return new class_name; }
     
     IMPLEMENT_RUNTIMECLASS(class_name, base_class_name, wSchema,
          class_name::CreateObject)
     
     CArchive& AFXAPI operator>>(CArchive& ar, class_name* &pOb)
     { pOb = (class_name*) ar.ReadObject(RUNTIME_CLASS(class_name));
    return ar; }

     AFX_CLASSINIT _init_##class_name(RUNTIME_CLASS(class_name));

Насчет DECLARE_SERIAL всё вроде ясно: включение знакомого нам DECLARE_DYNCREATE, и определение пресловутого оператора ">>". Первые два пункта IMPLEMENT_SERIAL тоже не представляют интереса (они полностью совпадают с макросом IMPLEMENT_DYNCREATE). А вот на следующих двух стоит остановиться подробнее.

Оператор ">>", как видим, вызывает метод "ar.ReadObject", передавая ему CRuntimeClass нашего "MyClassA". Это необходимо для того, чтобы убедиться, что загруженный из архива объект точно принадлежит классу "MyClassA", в противном случае генерится исключение "CArchiveException::badClass" (которое можно перехватить, но это уже тема для отдельного разговора). В случае успеха, указателю "pOb" присваивается адрес загруженного объекта. В процессе загрузки используется функция "Serialize", определенная в нашем классе "MyClassA", для того чтобы "раскидать" данные из архива по полям объекта.

В качестве простого примера эффективного использования механизма сериализации (вкупе с использованием STL), можно привести следующий код:
Код
// Test.cpp
class  MyClassA: public CObject
{
public:
      list<CObject*> xlist;
      int  i;
      CString str;

      void EmptyList()
      {
            for(list<CObject*>::iterator iter=xlist.begin(); iter!=xlist.end(); iter++)
                  delete (*iter);
            xlist.empty();
      }
      ~MyClassA()  {EmptyList();}

      virtual void Serialize(CArchive &ar);
      DECLARE_SERIAL(MyClassA)
};

IMPLEMENT_SERIAL(MyClassA, CObject, 0);

void MyClassA::Serialize(CArchive &ar)
{
      if(ar.IsStoring())   // если сохранение...
      {
            ar<<i;
            ar<<str;
            ar<<xlist.size();
            for(list<CObject*>::iterator iter=xlist.begin(); iter!=xlist.end(); iter++)
                  ar<<(*iter);
      }
      else   // загрузка из архива
      {
            ar>>i;
            ar>>str;
            EmptyList();
            int  newSize = 0;
            ar>>newSize;
            for(int  j=0; j<newSize; j++)
            {
                  CObject *ObjLoaded=NULL;
                  ar>>ObjLoaded;
                  xlist.push_back(ObjLoaded);
            }
      }
}


int main()
{
      MyClassA  *obj = NULL;

      // загрузим объект из архива, например, из файла "1.dat"
      try
      {
            CFile  f("c:\\1.dat", CFile::modeRead);
            CArchive a(&f, CArchive::load);
            ar>>obj;
      }
      catch(...) 
      {
            MessageBox(0, "Не удалось загрузить объект", "Error", MB_OK);
            return 0;
      }

      // ... какие-то изменения объекта "obj" ...

      // сохраним объект обратно в архив
      CFile f("c:\\1.dat", CFile::modeCreate|CFile::modeWrite);
      CArchive ar(&f, CArchive::store);
      ar<<obj;

      // удаляем объект, динамически созданный при загрузке из архива
      delete obj;

      return 1;
}


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

Осталась только одна нерассмотренная "фича" - глобальный список CRuntimeClass'ов (и связанная с ним странная конструкция "AFX_CLASSINIT _init_##class_name(RUNTIME_CLASS(class_name));")

Остановимся на примере из предыдущего поста (класс MyClassA, содержащий список объектов различных классов). Приведу кратко основные моменты, на которые нужно обратить внимание:
- объекты в списке xlist могут принадлежать произвольным классам;
- на момент чтения списка объектов из архива неизвестно, к какому классу будет относиться очередной загружаемый объект;
- в архиве перед каждым объектом хранится имя класса;
- соответственно, при чтении очередного объекта из архива, зная имя класса, MFC нужно каким-то образом создать объект данного класса.

Данная задача была решена очень просто: с помощью глобального списка CRuntimeClass'ов, хранимого внутри объекта CModuleState. Чтобы получить доступ к этому объекту, используется функция MFC "AfxGetModuleState", возвращающая указатель на объект CModuleState, в котором определено поле "m_classList", представляющее собой простой односвязный список. Перечислить все классы, "зарегистрированные" на данный момент, можно например так:
Код
list <char*> lstClassNames;
list <CRuntimeClass*> lstClasses;
AFX_MODULE_STATE* pModuleState = AfxGetModuleState();
for (CRuntimeClass *pClass = pModuleState->m_classList; 
       pClass != NULL; 
       pClass = pClass->m_pNextClass)
       {
              lstClasses.push_back(pClass);
              lstClassNames.push_back(pClass->m_lpszClassName);
       }


Список классов может пригодиться, например, в случае необходимости создать объект по имени класса (не имея "под рукой" CRuntimeClass). Кстати, такую фичу (создание объекта по строковому имени класса) включили в MFC 7.0 (идущую с .NET), в новой версии CRuntimeClass'а добавлено 4 специализированных метода:
Код

struct CRuntimeClass
{
       ...

       // dynamic name lookup and creation
       static CRuntimeClass* PASCAL FromName(LPCSTR lpszClassName);
       static CRuntimeClass* PASCAL FromName(LPCWSTR lpszClassName);
       static CObject* PASCAL CreateObject(LPCSTR lpszClassName);
       static CObject* PASCAL CreateObject(LPCWSTR lpszClassName);


Осталось только выяснить, каким образом CRuntimeClass попадает в список классов. Оказывается, не рассмотренный до сих пор кусок макроса IMPLEMENT_SERIAL и выполняет данную работу, вызывая глобальную функцию "AfxClassInit", добавляющую наш CRuntimeClass в список доступных классов:
Код
// IMPLEMENT_SERIAL
   ...
AFX_CLASSINIT _init_##class_name(RUNTIME_CLASS(class_name));

// где AFX_CLASSINIT - это структура:
struct AFX_CLASSINIT

     AFX_CLASSINIT(CRuntimeClass* pNewClass)  // конструктор
    { AfxClassInit(pNewClass); }  // функция AfxClassInit регистрирует наш класс
};



Вот и всё, что я хотел рассказать о "кухне" классов времени выполнения в MFC. 


--------------------
Цитата(Jimi Hendrix)
Well, I stand up next to a mountain
And I chop it down with the edge of my hand
PM MAIL WWW   Вверх
  
Ответ в темуСоздание новой темы Создание опроса
Правила форума "С++:Общие вопросы"
Earnest Daevaorn

Добро пожаловать!

  • Черновик стандарта C++ (за октябрь 2005) можно скачать с этого сайта. Прямая ссылка на файл черновика(4.4мб).
  • Черновик стандарта C (за сентябрь 2005) можно скачать с этого сайта. Прямая ссылка на файл черновика (3.4мб).
  • Прежде чем задать вопрос, прочтите это и/или это!
  • Здесь хранится весь мировой запас ссылок на документы, связанные с C++ :)
  • Не брезгуйте пользоваться тегами [code=cpp][/code].
  • Пожалуйста, не просите написать за вас программы в этом разделе - для этого существует "Центр Помощи".
  • C++ FAQ

Если Вам понравилась атмосфера форума, заходите к нам чаще! С уважением, Earnest Daevaorn

 
0 Пользователей читают эту тему (0 Гостей и 0 Скрытых Пользователей)
0 Пользователей:
« Предыдущая тема | C/C++: Общие вопросы | Следующая тема »


 




[ Время генерации скрипта: 0.1100 ]   [ Использовано запросов: 22 ]   [ GZIP включён ]


Реклама на сайте     Информационное спонсорство

 
По вопросам размещения рекламы пишите на vladimir(sobaka)vingrad.ru
Отказ от ответственности     Powered by Invision Power Board(R) 1.3 © 2003  IPS, Inc.