FAQ:STL:C++ сериализация данных — различия между версиями
LogRus (обсуждение | вклад) |
RXL (обсуждение | вклад) |
||
(не показано 7 промежуточных версий 3 участников) | |||
Строка 1: | Строка 1: | ||
Иногда нужно сохранить состояние класса в файл, передать состояние класса по сети. Подобные задачи обычно решает сериализация. | Иногда нужно сохранить состояние класса в файл, передать состояние класса по сети. Подобные задачи обычно решает сериализация. | ||
− | Говоря о сериализации, я | + | Говоря о сериализации, я подразумеваю механизмы перевода некоторого класса, структуры или набора переменных в определённый формат (бинарный, текстовый, XML, HTML и т.д.), а также сам процесс перевода. Десериализация - процессы и механизмы, обратные сериализации (восстановление состояния из внешнего источника). |
− | Самый простой способ, к которому чаще всего прибегают молодые программисты - это простое копирование памяти в файл или еще куда-то. Т.е. берём указатель на класс/структуру/переменную и копируем N байт в файл. Пример: <syntaxhighlight lang="cpp"> | + | Самый простой способ, к которому чаще всего прибегают молодые программисты, - это простое копирование памяти в файл или еще куда-то. Т.е. берём указатель на класс/структуру/переменную и копируем N байт в файл. Пример: <syntaxhighlight lang="cpp"> |
....... | ....... | ||
MyClass m; | MyClass m; | ||
Строка 15: | Строка 15: | ||
Но этот метод сериализации имеет ряд ограничений и недостатков: | Но этот метод сериализации имеет ряд ограничений и недостатков: | ||
− | *Допустимо использовать только для POD-структур (POD - Plain Old Data) и встроенных типов. Почему будет понятно из следующих пунктов. | + | *Допустимо использовать только для POD-структур (POD - Plain Old Data) и встроенных типов. Почему, будет понятно из следующих пунктов. |
− | *Если | + | *Если программистом описан конструктор, то компилятор вправе в класс добавить какие-то свои вспомогательные переменные, что превращает класс в не POD-структуру, на самом деле это не так страшно, но формально это так. |
− | *При сохранении указателей членов класса | + | *При сохранении указателей членов класса будут скопированы только адреса, хранимые указателями и, естественно, класс с указателями это не POD-тип |
− | *Если в классе объявленные виртуальные функции (или он унаследован от класса содержащего виртуальные функции), приводит к тому, что класс будет дополнен указателем на таблицу виртуальных функций, и с этим указателем | + | *Если в классе объявленные виртуальные функции (или он унаследован от класса содержащего виртуальные функции), это приводит к тому, что класс будет дополнен указателем на таблицу виртуальных функций, и с этим указателем та же проблема, что и со всеми другими. Опять же не POD-тип. |
*Если ваш класс содержит внутри себя не POD типы или унаследован от не POD-типа, то ваш класс тоже не под тип, т.е. нет никакой гарантии, что копирование куска памяти позволит постановить состояние класса. | *Если ваш класс содержит внутри себя не POD типы или унаследован от не POD-типа, то ваш класс тоже не под тип, т.е. нет никакой гарантии, что копирование куска памяти позволит постановить состояние класса. | ||
− | *Различное выравнивание данных внутри класса может сделать | + | *Различное выравнивание данных внутри класса может сделать невозможным перенос сохранённого класса на другую платформу или даже в программу, скомпилированную с другими параметрами компиляции. |
*Различный порядок байт не позволит переносить данные между такими платформами, как: x86 и PowerPC | *Различный порядок байт не позволит переносить данные между такими платформами, как: x86 и PowerPC | ||
− | *И такая сериализация не даёт сохранить в удобочитаемой форме | + | *И такая сериализация не даёт сохранить в удобочитаемой форме XML, текст или CSV |
− | Есть | + | Есть много ситуаций, когда создание дампа памяти - вполне допустимый метод сериализации, но также есть другое множество ситуаций, когда такой подход недопустим. |
Как только программист задумывается о сериализации данных, ему сразу же хочется выполнять сериализацию всего парой строк кода: легко и изящно, например, так: <syntaxhighlight lang="cpp"> | Как только программист задумывается о сериализации данных, ему сразу же хочется выполнять сериализацию всего парой строк кода: легко и изящно, например, так: <syntaxhighlight lang="cpp"> | ||
− | // вывести состояние | + | // вывести состояние класса и всех его членов. |
std::cout << myClass; | std::cout << myClass; | ||
Строка 34: | Строка 34: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | И естественно самый простой способ быстро добиться результата это использовать велосипед написанный другими. Велосипед возьмём хороший | + | И, естественно, самый простой способ быстро добиться результата - это использовать "велосипед", написанный другими. "Велосипед" возьмём хороший, многофункциональный. Он умеет выполнять сериализацию и десериализацию стандартных контейнеров, классов, указателей, ссылок и еще чего-то. Также он умеет сохранять, работать с различными форматами выходных данных: бинарный, текст, XML. Если очень хочется, то он может сохранить не только в поток, но и куда угодно, например, в вектор или в сокет или выкинуть в пропасть. |
− | Полное описание велосипеда: http://www.boost.org/doc/libs/1_36_0/libs/serialization/doc/index.html | + | Полное описание "велосипеда": http://www.boost.org/doc/libs/1_36_0/libs/serialization/doc/index.html |
Вот пример использования (взято из описания): | Вот пример использования (взято из описания): | ||
Строка 99: | Строка 99: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | Теперь по пунктам как это работает: | + | Теперь по пунктам, как это работает: |
− | # | + | #Создаём внутри нашего класса метод serialize, получаем ссылку на архив и номер версии(можно по-разному сериализовать в зависимости от версии), если метод приватный, то добавляем в друзья class boost::serialization::access. Метод serialize будет вызываться при сериализации и десериализации. |
− | # | + | #Открываем файл и создаём архив (text_oarchive текстовый выходной архив), в нашем случае текстовый, архив - это тот самый класс, который выполняет основную работу. |
− | # | + | #Вызываем всеми любимый оператор << - этот оператор вызывает метод serialize для классов или же внешние функции(они идут в комплекте) для встроенных типов и стандартных контейнеров. |
− | #text_oarchive::operator<< вызвал наш метод serialize и передал | + | #text_oarchive::operator<< вызвал наш метод serialize и передал вовнутрь себя, тут возникает вопрос: почему внутри serialize используется оператор &, а не <<? Ответ: потому что у выходного архива операторы & и << по сути это одно и тоже, у входного операторы & и >> - одно и тоже. Т.е. ничто не мешает в коде поменять "ia >> newg;" на "ia & newg;". |
− | # | + | #Если нужно изменить метод сериализации, достаточно поменять тип архива (для XML архива придётся сделать еще некоторою работу в методе serialize). |
− | + | На этом, в общем-то, работа по поддержке сериализации закончена. | |
− | При желании, можно разделить методы для сериализации и десериализации. Кстати boost::seralization гарантирует, что порядок байт при сериализации будет изменён, если потребуется, так что можно спокойно передать long с x86 на PowerPC. | + | При желании, можно разделить методы для сериализации и десериализации. Кстати, boost::seralization гарантирует, что порядок байт при сериализации будет изменён, если потребуется, так что можно спокойно передать long с x86 на PowerPC. |
− | Немного поправленный | + | Немного поправленный пример использования и результаты работы: |
<syntaxhighlight lang="cpp"> | <syntaxhighlight lang="cpp"> | ||
Строка 128: | Строка 128: | ||
#include <boost/archive/xml_iarchive.hpp> | #include <boost/archive/xml_iarchive.hpp> | ||
#include <boost/archive/xml_oarchive.hpp> | #include <boost/archive/xml_oarchive.hpp> | ||
− | // включаем, | + | // включаем, чтобы сериализация работала с векторами |
#include <boost/serialization/vector.hpp> | #include <boost/serialization/vector.hpp> | ||
− | // | + | // включаем, чтобы нормально проходила сериализация XML |
#include <boost/serialization/nvp.hpp> | #include <boost/serialization/nvp.hpp> | ||
Строка 141: | Строка 141: | ||
void serialize(Archive & ar, const unsigned int version) | void serialize(Archive & ar, const unsigned int version) | ||
{ | { | ||
− | // | + | // то же, что и make_nvp, только имя параметра выводится в макросе |
ar & BOOST_SERIALIZATION_NVP(degrees); | ar & BOOST_SERIALIZATION_NVP(degrees); | ||
ar & BOOST_SERIALIZATION_NVP(minutes); | ar & BOOST_SERIALIZATION_NVP(minutes); | ||
Строка 163: | Строка 163: | ||
std::ofstream ofs(file.c_str(), std::ios::out|flags); | std::ofstream ofs(file.c_str(), std::ios::out|flags); | ||
TOArch oa(ofs); | TOArch oa(ofs); | ||
− | // make_nvp создаёт пару имя- | + | // make_nvp создаёт пару имя-значение, которая отразится в XML |
// если не используем XML архив, то можно пару не создавать | // если не используем XML архив, то можно пару не создавать | ||
oa << boost::serialization::make_nvp("Test_Object", cont); | oa << boost::serialization::make_nvp("Test_Object", cont); | ||
Строка 175: | Строка 175: | ||
} | } | ||
− | { // Еще раз сериализуем, | + | { // Еще раз сериализуем, чтобы потом сравнить результаты двух сериализаций |
− | // и | + | // и убедиться, что десериализациия прошла корректно |
std::ofstream ofs((file+".tmp").c_str(), std::ios::out|flags); | std::ofstream ofs((file+".tmp").c_str(), std::ios::out|flags); | ||
TOArch oa(ofs); | TOArch oa(ofs); | ||
Строка 201: | Строка 201: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | Файл text_arch.dump: < | + | Файл text_arch.dump: <pre>22 serialization::archive 3 0 0 3 0 0 35 59 24.566999 36 60 25.566999 37 61 26.566999 |
− | 22 serialization::archive 3 0 0 3 0 0 35 59 24.566999 36 60 25.566999 37 61 26.566999 | + | </pre> |
− | </ | + | |
− | Как видите | + | Как видите, использование NVP никак не отразилось на внешнем виде архива. |
− | Файл xml_arch.dump: <syntaxhighlight> | + | Файл xml_arch.dump: <syntaxhighlight lang="xml"> |
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?> | <?xml version="1.0" encoding="UTF-8" standalone="yes" ?> | ||
<!DOCTYPE boost_serialization> | <!DOCTYPE boost_serialization> | ||
Строка 232: | Строка 231: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | Бинарный архив приводить не стану :) не очень красочно, но занимает он 79 байт из которых 39 заголовок и 40 полезная информация. | + | Бинарный архив приводить не стану :) не очень красочно, но занимает он 79 байт, из которых 39 - заголовок и 40 - полезная информация. |
− | + | ||
+ | <br> | ||
За универсальность boost::serialization придётся заплатить: | За универсальность boost::serialization придётся заплатить: | ||
− | * | + | *Во время компиляции шаблоны могут разворачиваться довольно долго. |
− | *Скорость: стек вызовов для сериализации какой- | + | *Скорость: стек вызовов для сериализации какой-нибудь не слишком больной структурки, может быть просто ужасающим - вызовов 20-30. |
− | Но | + | Но если вы не пишете систему массового обслуживания, то это то, что вам нужно, с помощью этой библиотеки можно даже реализовать маршалинг или RPC. |
− | <br> Надеюсь в общих чертах | + | <br> Надеюсь, в общих чертах понятно, как примерно работает сериализация и десериализация, и если понадобится, можно реализовать свою сериализацию. |
− | <br> Вот пример своей реализации архива, который я использую вместо boost:binary_iarchive (была | + | <br> Вот пример своей реализации архива, который я использую вместо boost:binary_iarchive (была ОЧЕНЬ важна скорость, а универсальность не очень, но для маршалинга хватает), делал так, чтобы можно было использовать один вместо другого без переделки кода: |
<syntaxhighlight lang="cpp"> | <syntaxhighlight lang="cpp"> | ||
Строка 304: | Строка 303: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | Кое-что порезал, | + | Кое-что порезал, чтобы не расслаблялись :) |
[[Category:FAQ]] [[Category:FAQ:ANSI_CPP]] [[Category:FAQ:STL]] [[Category:FAQ:STL_BOOST]] | [[Category:FAQ]] [[Category:FAQ:ANSI_CPP]] [[Category:FAQ:STL]] [[Category:FAQ:STL_BOOST]] |
Текущая версия на 01:41, 4 августа 2015
Иногда нужно сохранить состояние класса в файл, передать состояние класса по сети. Подобные задачи обычно решает сериализация.
Говоря о сериализации, я подразумеваю механизмы перевода некоторого класса, структуры или набора переменных в определённый формат (бинарный, текстовый, XML, HTML и т.д.), а также сам процесс перевода. Десериализация - процессы и механизмы, обратные сериализации (восстановление состояния из внешнего источника).
Самый простой способ, к которому чаще всего прибегают молодые программисты, - это простое копирование памяти в файл или еще куда-то. Т.е. берём указатель на класс/структуру/переменную и копируем N байт в файл. Пример:....... MyClass m; .............. std::ofstream f("dump.bin", std::binary); f.write(&m, sizeof(m)); f.close(); ........................
Но этот метод сериализации имеет ряд ограничений и недостатков:
- Допустимо использовать только для POD-структур (POD - Plain Old Data) и встроенных типов. Почему, будет понятно из следующих пунктов.
- Если программистом описан конструктор, то компилятор вправе в класс добавить какие-то свои вспомогательные переменные, что превращает класс в не POD-структуру, на самом деле это не так страшно, но формально это так.
- При сохранении указателей членов класса будут скопированы только адреса, хранимые указателями и, естественно, класс с указателями это не POD-тип
- Если в классе объявленные виртуальные функции (или он унаследован от класса содержащего виртуальные функции), это приводит к тому, что класс будет дополнен указателем на таблицу виртуальных функций, и с этим указателем та же проблема, что и со всеми другими. Опять же не POD-тип.
- Если ваш класс содержит внутри себя не POD типы или унаследован от не POD-типа, то ваш класс тоже не под тип, т.е. нет никакой гарантии, что копирование куска памяти позволит постановить состояние класса.
- Различное выравнивание данных внутри класса может сделать невозможным перенос сохранённого класса на другую платформу или даже в программу, скомпилированную с другими параметрами компиляции.
- Различный порядок байт не позволит переносить данные между такими платформами, как: x86 и PowerPC
- И такая сериализация не даёт сохранить в удобочитаемой форме XML, текст или CSV
Есть много ситуаций, когда создание дампа памяти - вполне допустимый метод сериализации, но также есть другое множество ситуаций, когда такой подход недопустим.
Как только программист задумывается о сериализации данных, ему сразу же хочется выполнять сериализацию всего парой строк кода: легко и изящно, например, так:// вывести состояние класса и всех его членов. std::cout << myClass; // загрузить состояние класса из XML myXML >> myClass;
И, естественно, самый простой способ быстро добиться результата - это использовать "велосипед", написанный другими. "Велосипед" возьмём хороший, многофункциональный. Он умеет выполнять сериализацию и десериализацию стандартных контейнеров, классов, указателей, ссылок и еще чего-то. Также он умеет сохранять, работать с различными форматами выходных данных: бинарный, текст, XML. Если очень хочется, то он может сохранить не только в поток, но и куда угодно, например, в вектор или в сокет или выкинуть в пропасть.
Полное описание "велосипеда": http://www.boost.org/doc/libs/1_36_0/libs/serialization/doc/index.html
Вот пример использования (взято из описания):
///////////////////////////////////////////////////////////// // gps координаты // // illustrates serialization for a simple type // class gps_position { private: friend class boost::serialization::access; // When the class Archive corresponds to an output archive, the // & operator is defined similar to <<. Likewise, when the class Archive // is a type of input archive the & operator is defined similar to >>. template<class Archive> void serialize(Archive & ar, const unsigned int version) { ar & degrees; ar & minutes; ar & seconds; } int degrees; int minutes; float seconds; public: gps_position(){}; gps_position(int d, int m, float s) : degrees(d), minutes(m), seconds(s) {} }; int main() { // create and open a character archive for output std::ofstream ofs("filename"); // create class instance const gps_position g(35, 59, 24.567f); // save data to archive { boost::archive::text_oarchive oa(ofs); // write class instance to archive oa << g; // archive and stream closed when destructors are called } // ... some time later restore the class instance to its orginal state gps_position newg; { // create and open an archive for input std::ifstream ifs("filename"); boost::archive::text_iarchive ia(ifs); // read class state from archive ia >> newg; // archive and stream closed when destructors are called } return 0; }
Теперь по пунктам, как это работает:
- Создаём внутри нашего класса метод serialize, получаем ссылку на архив и номер версии(можно по-разному сериализовать в зависимости от версии), если метод приватный, то добавляем в друзья class boost::serialization::access. Метод serialize будет вызываться при сериализации и десериализации.
- Открываем файл и создаём архив (text_oarchive текстовый выходной архив), в нашем случае текстовый, архив - это тот самый класс, который выполняет основную работу.
- Вызываем всеми любимый оператор << - этот оператор вызывает метод serialize для классов или же внешние функции(они идут в комплекте) для встроенных типов и стандартных контейнеров.
- text_oarchive::operator<< вызвал наш метод serialize и передал вовнутрь себя, тут возникает вопрос: почему внутри serialize используется оператор &, а не <<? Ответ: потому что у выходного архива операторы & и << по сути это одно и тоже, у входного операторы & и >> - одно и тоже. Т.е. ничто не мешает в коде поменять "ia >> newg;" на "ia & newg;".
- Если нужно изменить метод сериализации, достаточно поменять тип архива (для XML архива придётся сделать еще некоторою работу в методе serialize).
На этом, в общем-то, работа по поддержке сериализации закончена.
При желании, можно разделить методы для сериализации и десериализации. Кстати, boost::seralization гарантирует, что порядок байт при сериализации будет изменён, если потребуется, так что можно спокойно передать long с x86 на PowerPC.
Немного поправленный пример использования и результаты работы:
#include "stdafx.h" #include <iostream> #include <fstream> #include <string> #include <fstream> #include <vector> // include headers that implement a archive in simple text format #include <boost/archive/text_oarchive.hpp> #include <boost/archive/text_iarchive.hpp> #include <boost/archive/binary_iarchive.hpp> #include <boost/archive/binary_oarchive.hpp> #include <boost/archive/xml_iarchive.hpp> #include <boost/archive/xml_oarchive.hpp> // включаем, чтобы сериализация работала с векторами #include <boost/serialization/vector.hpp> // включаем, чтобы нормально проходила сериализация XML #include <boost/serialization/nvp.hpp> class gps_position { private: friend class boost::serialization::access; template<class Archive> void serialize(Archive & ar, const unsigned int version) { // то же, что и make_nvp, только имя параметра выводится в макросе ar & BOOST_SERIALIZATION_NVP(degrees); ar & BOOST_SERIALIZATION_NVP(minutes); ar & BOOST_SERIALIZATION_NVP(seconds); } int degrees; int minutes; float seconds; public: gps_position(){}; gps_position(int d, int m, float s) : degrees(d), minutes(m), seconds(s) {} }; template<typename TIArch, typename TOArch, typename TClass> void TestArch(const std::string & file, int flags, const TClass & cont) { { // Сериализуем std::ofstream ofs(file.c_str(), std::ios::out|flags); TOArch oa(ofs); // make_nvp создаёт пару имя-значение, которая отразится в XML // если не используем XML архив, то можно пару не создавать oa << boost::serialization::make_nvp("Test_Object", cont); } TClass newg; { // Десериализуем std::ifstream ifs(file.c_str(), std::ios::in|flags); TIArch ia(ifs); ia >> boost::serialization::make_nvp("Test_Object",newg); } { // Еще раз сериализуем, чтобы потом сравнить результаты двух сериализаций // и убедиться, что десериализациия прошла корректно std::ofstream ofs((file+".tmp").c_str(), std::ios::out|flags); TOArch oa(ofs); oa << boost::serialization::make_nvp("Test_Object", cont); } } int _tmain(int argc, _TCHAR* argv[]) { std::ofstream ofs("filename"); std::vector<gps_position> v; v.push_back(gps_position(35, 59, 24.567f)); v.push_back(gps_position(36, 60, 25.567f)); v.push_back(gps_position(37, 61, 26.567f)); using namespace boost::archive; TestArch<text_iarchive, text_oarchive>("text_arch.dump", 0, v); TestArch<binary_iarchive, binary_oarchive>("binary_arch.dump", std::ios::binary, v); TestArch<xml_iarchive, xml_oarchive>("xml_arch.dump", 0, v); return 0; }
22 serialization::archive 3 0 0 3 0 0 35 59 24.566999 36 60 25.566999 37 61 26.566999
Как видите, использование NVP никак не отразилось на внешнем виде архива.
Файл xml_arch.dump:<?xml version="1.0" encoding="UTF-8" standalone="yes" ?> <!DOCTYPE boost_serialization> <boost_serialization signature="serialization::archive" version="3"> <Test_Object class_id="0" tracking_level="0" version="0"> <count>3</count> <item class_id="1" tracking_level="0" version="0"> <degrees>35</degrees> <minutes>59</minutes> <seconds>24.566999</seconds> </item> <item> <degrees>36</degrees> <minutes>60</minutes> <seconds>25.566999</seconds> </item> <item> <degrees>37</degrees> <minutes>61</minutes> <seconds>26.566999</seconds> </item> </Test_Object> </boost_serialization>
Бинарный архив приводить не стану :) не очень красочно, но занимает он 79 байт, из которых 39 - заголовок и 40 - полезная информация.
За универсальность boost::serialization придётся заплатить:
- Во время компиляции шаблоны могут разворачиваться довольно долго.
- Скорость: стек вызовов для сериализации какой-нибудь не слишком больной структурки, может быть просто ужасающим - вызовов 20-30.
Но если вы не пишете систему массового обслуживания, то это то, что вам нужно, с помощью этой библиотеки можно даже реализовать маршалинг или RPC.
Надеюсь, в общих чертах понятно, как примерно работает сериализация и десериализация, и если понадобится, можно реализовать свою сериализацию.
Вот пример своей реализации архива, который я использую вместо boost:binary_iarchive (была ОЧЕНЬ важна скорость, а универсальность не очень, но для маршалинга хватает), делал так, чтобы можно было использовать один вместо другого без переделки кода:
class binary_iarchive { public: typedef serialization::container container; typedef container::iterator iterator; container &cont_; size_t currentPos_; typedef boost::mpl::bool_<false> is_saving; binary_iarchive(container & cont, long = 0) : cont_(cont) , currentPos_(0) { } template<typename T> binary_iarchive & operator>>(T & val) { deserialize_impl(val); return *this; } void reset() { resetPos(); cont_.clear(); } template<typename T> inline void raw_read(T beginPos, size_t len) { if (static_cast<size_t>(cont_.size() - currentPos_) < len) throw std::runtime_error("No more data"); iterator pos = cont_.begin() + currentPos_; iterator endPos = pos + len; std::copy(pos, endPos, beginPos); currentPos_ = currentPos_ + len; } private: // Fundamental template<typename T> inline void deserialize_impl(T & val, typename boost::enable_if<boost::is_fundamental<T> >::type* dummy = 0) { raw_read(reinterpret_cast<char*>(&val), sizeof(T)); } //Classes template<typename T> inline void deserialize_impl(T & val, typename boost::enable_if<boost::is_class<T> >::type* dummy = 0) { deserialize_class(*this, val); } };
Кое-что порезал, чтобы не расслаблялись :)