FAQ:STL:C++ сериализация данных
Иногда нужно сохранить состояние класса в файл, передать состояние класса по сети. Подобные задачи обычно решает сериализация.
Говоря о сериализации, я подразумиваю механизмы перевода некоторого класса, структуры или набора переменных в определённый формат (бинарный, текстовый, 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. Что он делает он в сериализатор(в терминах boost это archive) передаёт все внутренние данные, в свою очередь архив для простых типов делает копирование в памяти (кстати следит при этом за порядком байт, так что можно спокойно переносить на файл на другую машину с другой архитектурой), для сложных зовёт метод serialize, для стандартных контейнеров тоже, есть методы сериализации для контейнеров простых типов происходит копирование элементов, для контейнеров сложных типов происходит вызов serialize для каждого элемента. Для сериализации контейнеров придётся подключить дополнительные заголовки.
Можно разделить методы для сериализации и десериализации, есть поддержка версий архивов, есть макросы для подписи элементов в текстовом файле и XML есть примитивы для создания своих форматов (или я повторяюсь) и еще много чего, классная библиотека.
Но за универсальность boost::serialization придётся заплатить:
- Время компиляции шаблоны могут разворачиваться довольно долго
- Скорость: стек вызовов для сериализации какой-нибуть не слишком больной структурки может быть просто ужасающим вызовов 20-30.
Но, если вы не пишете систему массового обслуживания, то это то, что вам нужно с помощью этой библиотеки можно даже реализовать маршалинг или RPC, как больше нравится, т.е. я в своей программе создаю класс, но вместо собственно класса получаю gate к этому классу созданному на другой машине в момент когда я его создал на своей и я могу работать с созданным на другой машине классом, как с созданным на своей, при этом спокойно пользуюсь вызовами с передачей параметров по ссылке или указателю и знаю, что всё будет сериализовано - передано - обработано - передано обратно - десериализовано, так, что я не замечу подмены.
Понятно, как примерно работает сериализация и десериализация, и если понадобится можно реализовать свою сериализацию.
Можно, например, реализовать у объектов метод ToString который будет звать такой же метод у всех своих потомком, что бы сериализовать объект в строку.
Вот пример своей реализации архива, который я использую вместо бустового (ибо скорость ОЧЕНЬ важна, а универсальность не очень, но для маршалинга хватает), но делал так, что бы можно было использовать один вместо другого без переделки кода:
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); } };
Кое-что порезал, что бы не расслаблялись :)