FAQ:STL:C++ сериализация данных — различия между версиями
RXL (обсуждение | вклад) м (Формат code=xml.) |
RXL (обсуждение | вклад) |
||
(не показаны 3 промежуточные версии этого же участника) | |||
Строка 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 никак не отразилось на внешнем виде архива. | Как видите, использование NVP никак не отразилось на внешнем виде архива. |
Текущая версия на 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); } };
Кое-что порезал, чтобы не расслаблялись :)