воскресенье, 2 декабря 2012 г.

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);
 }
};
Кое-что порезал, чтобы не расслаблялись :)

Комментариев нет:

Отправить комментарий