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;
}

Теперь по пунктам, как это работает:

  1. Создаём внутри нашего класса метод serialize, получаем ссылку на архив и номер версии(можно по-разному сериализовать в зависимости от версии), если метод приватный, то добавляем в друзья class boost::serialization::access. Метод serialize будет вызываться при сериализации и десериализации.
  2. Открываем файл и создаём архив (text_oarchive текстовый выходной архив), в нашем случае текстовый, архив - это тот самый класс, который выполняет основную работу.
  3. Вызываем всеми любимый оператор << - этот оператор вызывает метод serialize для классов или же внешние функции(они идут в комплекте) для встроенных типов и стандартных контейнеров.
  4. text_oarchive::operator<< вызвал наш метод serialize и передал вовнутрь себя, тут возникает вопрос: почему внутри serialize используется оператор &, а не <<? Ответ: потому что у выходного архива операторы & и << по сути это одно и тоже, у входного операторы & и >> - одно и тоже. Т.е. ничто не мешает в коде поменять "ia >> newg;" на "ia & newg;".
  5. Если нужно изменить метод сериализации, достаточно поменять тип архива (для 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;
}
Файл text_arch.dump:
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);
}
};

Кое-что порезал, чтобы не расслаблялись :)