Сообщений 13    Оценка 70        Оценить  
Система Orphus

Класс для сериализации CSerializeBase

Автор: Сабельников Андрей Николаевич
Источник: RSDN Magazine #6-2004
Опубликовано: 19.03.2005
Исправлено: 10.12.2016
Версия текста: 1.0
Для чего?
Как использовать

Демонстрационный проект

Для чего?

Вопросы сохранения данных из объектов так или иначе возникают у каждого разработчика. В какой-то момент появляется желание “упаковать” все (или не все) данные какого-нибудь объекта и просто сохранить их в файл, или передать по сети и т.п. Это довольно просто сделать для так называемых POD-типов (plain old data) – с помощью копирования соответствующих участков памяти. Но если в структуре появляется, к примеру, хотя бы указатель на строку, то этот метод совершенно не годится. Приходится определять формат, отлаживать его, документировать, и делать разные другие нехорошие вещи.

Итак, необходим инструмент, с помощью которого можно “упаковывать” любой объект класса С++ в непрерывный кусок памяти. Предлагаю вариант, который, я надеюсь, поможет многим сэкономить время.

Как использовать

Предположим, у вас имеется некоторая простая структура классов:

      class A
{
  int m_iSomeCount;
  DWORD m_dwSomeCount;
  UINT m_uSomeUnsigned;
  CString m_sSomeStr1;
  CString m_sSomeStr2;
public:
  A(){}
  virtual ~A(){}
  //some functions//.....
};


class B
{
  A m_a;
  UINT m_uSomeCount2;
  std::vector<A> m_VectorOfA;
  std::list<int> m_listOfInt;
public:
  B(){}
  virtual ~B(){}
  //some functions//.....
};

И вы непременно хотите уметь легко сохранять объекты типа B. Первая сложность заключается в том, что в A:: содержится два объекта CString, вторая - в том, что в B:: содержится, во-первых, std::vector<A> (что может быть не самым страшным, т.к. вектор по стандарту должен располагаться в памяти линейно одним сегментом), а во-вторых, std::list<int> (что более неприятно, т.к. list, скорее всего, будет разбросан по разным участкам кучи).

Вот три вещи, которые необходимо сделать, чтобы использовать CSerializeBase:

Включить заголовочный файл SerializeBase.h:

Отнаследовать классы от CserializeBase.

Написать карту сериализации.

Вот как теперь будут выглядеть объявления классов A и B:

      #include "SerializeBase.h"

      //...
      class A: public CSerializeBase
{
  int m_iSomeCount;
  DWORD m_dwSomeCount;
  UINT m_uSomeUnsigned;
  CString m_sSomeStr1;
  CString m_sSomeStr2;

public:
  A(){}
  virtual ~A(){}

  BEGIN_SERIALIZE_MAP(A)
    SERIALIZE_NATIVE(m_iSomeCount)
    SERIALIZE_NATIVE(m_dwSomeCount)
    SERIALIZE_NATIVE(m_uSomeUnsigned)
    SERIALIZE_CSTRING_AS_UNICODE(m_sSomeStr1)
    SERIALIZE_CSTRING_AS_ASCII(m_sSomeStr2)
  END_SERIALIZE_MAP()
};

class B: public CSerializeBase
{
  A m_a;
  UINT m_uSomeCount2;
  vector_in_T_serializable<A> m_VectorOfA;
  list_in_native_serializable<int> m_listOfInt;
public:
  B(){}
  virtual ~B(){}

  BEGIN_SERIALIZE_MAP(B)
    SERIALIZE_NATIVE(m_uSomeCount2)
    SERIALIZE_T(&m_a)
    SERIALIZE_T(&m_VectorOfA)
    SERIALIZE_T(&m_listOfInt)
  END_SERIALIZE_MAP()

};

Каждый член, который вы планируете включить в сериализацию, должен быть описан в карте.

ПРЕДУПРЕЖДЕНИЕ

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

Остановимся на карте сериализации:

BEGIN_SERIALIZE_MAP(A) и END_SERIALIZE_MAP() Открывает и закрывает карту сериализации.
SERIALIZE_NATIVE(varialble) Включается для переменной являющейся POD-типом.
SERIALIZE_CSTRING_AS_ASCII(varialble) Включается для переменной с типом CString, которая должна быть сохранена в однобайтовом формате ASCII.
SERIALIZE_CSTRING_AS_UNICODE(varialble) Включается для переменной с типом CString, которая должна быть сохранена в формате UNICODE.
SERIALIZE_T(pvarialble) Включается для указателя на переменную, с любым типом, наследованным от CSerializeBase.
СОВЕТ

Благодаря последнему элементу карты становится возможным рекурсивное сохранение сколь угодно сложных структур данных. Т.е. рассмотренный выше класс B может быть агрегирован в другой класс, и вставлен в его карту сериализации с помощью SERIALIZE_T(pvarialble).

Вы также можете расширить набор макросов карты по своему усмотрению, к примеру, внести туда поддержку std::string и т.п. (лично я больше люблю использовать CString из ATL, но если вам по ками-то причинам нужно использовать именно std::string, это не составит особого труда).

В тестовом проекте приведён рассмативаемый класс B и, собственно, сами файлы класса.

Ещё несколько слов о том ,что произошло с std::vector и с std::list.

...
class B: public CSerializeBase
{
  A m_a;
  UINT m_uSomeCount2;
  vector_in_T_serializable<A> m_VectorOfA;
  list_in_native_serializable<int> m_listOfInt;
public:
  B(){}
  virtual ~B(){}
...

В файле SerializeBase.h реализованы также несколько вспомогательных классов-контейнеров, которые могут очень пригодиться:

template<class T> class list_in_T_serializable Список, инстанируется типом, наследованным от CSerializeBase
template<class T> class list_in_native_serializable Список, инстанируется POD типом
template<class T> class vector_in_T_serializable Вектор, инстанируется типом, наследованным от CSerializeBase
template<class T> class vector_in_native_serializable Вектор, инстанируется POD типом

Все они открыто отнаследованны от своих прообразов из std:: а так же от класса CSerializeBase, поэтому могут участвовать в карте сериализации через макрос SERIALIZE_T(pvarialble). Реализация их довольно тривиальна, как я уже сказал вы, сможете её найти в файле SerializeBase.h. В общем случае раcширение функциональности может быть реализовано двумя различными путями. Первый – это расширение набора макросов карт сериализации: вы определяете свой дополнительный макрос, называющийся, например, SERIALIZE_CLASS_WIDGET(varialble), и в нём реализуете упаковку (аналогично уже написанным макросам). Второй, на мой взгляд, более удобный, - это когда вы наследуете класс от CSerializeBase и уже в нём определяете свою карту сообщений, точно так же, как было описано выше, или сами реализуете виртуальные функции Serialize(….) и UnSerialize(….), как это сделано для вспомогательных классов-контейнеров list_in_T_serializable и т.п. Кроме этого, в файле SerializeBase.h вы обнаружите ещё два класса CSerATString и CSerUTString. Оба этих класса реализуют интерфейс Iserialize (т.е. функции Serialize(….) и UnSerialize(….)), что даёт вам возможность инстанировать ими контайнеры, о которых только что говорилось. Разница лишь в том, что CSerATString упаковывается в ASCII кодировке, а CSerUTString – в кодировке UNICODE.

Теперь давайте посмотрим, как можно сохранять и загружать объекты типа B:

  B b1,b2;
  //Устанавливаем значения переменных-членов для b1//...

  CSerializedDownContainer down_container;
  CSerializedUpContainer upContainer;
  int count = b1.Serialize(&down_container);
  down_container.SerializeAllTo(&upContainer);
//Теперь вы можете делать с данными всё, что угодно, например, сохранить в файл или послать по сети//  BYTE* pBuf = upContainer.Get(); //так получаете указатель на буфер//  UINT bufferLen = upContainer.GetLength();//так получаете его длину//  --------------------------------------------------------- //  При получении данных (из-сети, или из файла) загружаете обратно в классы таким образом: //     upContainer.Set(const BYTE* pBuf, UINT lenght);//сначала копируете их в контейнер//          (или приатачиваетесь через функцию CSerializedUpContainer::Attach(BYTE* pBuf, UINT lenght))//После этого вызываете  UnSerialize у объекта в который должны быть загруженны данные.  b2.UnSerialize(&upContainer);

Класс CSerializedDownContainer – это контейнер, который после вызова b1.Serialize(&down_container) будет хранить у себя каждый элемент иерархии, участвующий в сериализации, в элементе списка. А после вызова down_container.SerializeAllTo(&upContainer) - все элементы будут последовательно, друг за другом, скопированы из down_container в непрерывный кусок памяти в upContainer.

ПРИМЕЧАНИЕ

Я ввёл класс CSerializedDownContainer, чтобы избежать потери производительности, связанной с realloc. Поскольку в момент, когда начинается сериализация, неизвестно, сколько займёт в итоге весь объект – 100 байт или 1Мбайт, то было бы нерационально перевыделять память каждый раз, когда в буфер не влезает очередной элемент (например, если буфер был длиной 1 Мбайт, и очередной элемент не поместился, то, если менеджер памяти не смог бы выделить память по этому адресу, пришлось бы копировать весь буфер – аж 1Мегабайт - в другое место, и так далее). Вместо этого CSerializedDownContainer сохраняет каждый элемент в узле списка. А когда сериализация окончена, вызывается down_container.SerializeAllTo(&upContainer), где подсчитывается размер буфера, необходимый, чтобы принять все элементы, выделяется память, и после этого начинается последовательный обход списка и копирование всех его элементов. Кроме того, в случае необходимости, список можно будет заменить, к примеру, на дерево и сохранять в формате XML, оформив всё это в виде стратегий сохранения.

В классе CSerializeBase есть ещё четыре виртуальные функции:

      virtual
      void OnPreSerialize(){} 
  virtualvoid OnEndSerialize(){}
  virtualvoid OnPreUnSerialize(){}
  virtualvoid OnEndUnSerialize(){}

Вы можете их определить в своём классе , чтобы обрабатывать соответствующие события.

ПРИМЕЧАНИЕ

Нужно иметь в виду, что при раскрутке буфера (unsеrialize) не ведётся никакого контроля ошибок. Если имеется существенная возможность повреждения данных, то самым лучшим дополнением будет завернуть упакованные данные в CRC-обертку и обрабатывать возникновение ошибок соответствющим образом.

В тестовом проекте вы найдёте работающий пример, который иллюстрирует основные аспекты работы CSerializeBase.

Если вас по каким-то причинам не удовлетворяет представленное решение, вы можете написать мне письмо с предложениями и пожеланиями, или посмотреть в сторону других решений, например в сторону boost::serialization или XTL.


Эта статья опубликована в журнале RSDN Magazine #6-2004. Информацию о журнале можно найти здесь
    Сообщений 13    Оценка 70        Оценить