Сообщений 13 Оценка 70 Оценить |
Для чего? Как использовать |
Вопросы сохранения данных из объектов так или иначе возникают у каждого разработчика. В какой-то момент появляется желание “упаковать” все (или не все) данные какого-нибудь объекта и просто сохранить их в файл, или передать по сети и т.п. Это довольно просто сделать для так называемых 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.
Сообщений 13 Оценка 70 Оценить |