FAQ:STL:C++ сериализация данных — различия между версиями

Материал из Весельчак У
Перейти к: навигация, поиск
м (Поправлены категории.)
Строка 1: Строка 1:
Периодически возникает потребность, сохранить состояние класса в файл или передать по сети или наоборот зачитать/получить. Подобные задачи обычно решает сериализация, можно конечно сделать простое копирование памяти, но тут естественно возникают ряд проблем:
+
Иногда нужно сохранить состояние класса в файл, передать состояние класса по сети. Подобные задачи обычно решает сериализация.
  
*Выравнивание членов касса или структуры, загрузить класс в другой библиотеке, где выравнивание отключено уже не получится
+
Говоря о сериализации, я подразумиваю механизмы перевода некоторого класса, структуры или набора переменных в определённый формат (бинарный, текстовый, XML, HTML и т.д.), а так же сам процесс перевода. Десериализация - процессы и механизмы обратные сериализации (восстановление состояния из внешнего источника).
*В гетерогенных средах порядок байт может другим: x86 и PowerPC
+
 
*Нельзя сохранять, что-то кроме POD структур память на которую ссылаются указатели, таким образом не скопируем.  
+
Самый простой способ, к которому чаще всего прибегают молодые программисты - это простое копирование памяти в файл или еще куда-то.
*Нельзя сохранить в Humanreadable форме.
+
Т.е. берём указатель на класс/структуру/переменную и копируем N байт в файл. Пример:
 +
<syntaxhighlight lang="cpp">
 +
.......
 +
MyClass m;
 +
..............
 +
std::ofstream f("dump.bin", std::binary);
 +
f.write(&m, sizeof(m));
 +
f.close();
 +
........................
 +
</syntaxhighlight>
 +
 
 +
Но этот метод сериализации имеет ряд ограничений и недостатков:
 +
*Допустимо использовать только для POD-структур (POD - Plain Old Data) и встроенных типов. Почему будет понятно из следующих пунктов.
 +
*Если, программистом описан конструктор, то компилятор в праве в класс добавить какие-то свои вспомогательные переменные, что превращает класс в не POD-структуру, на самом деле это не так страшно, но формально это так.
 +
*При сохранении указателей членов класса, будут скопированы, только адреса хранимые указателями и естественно класс с указателями это не POD-тип
 +
*Если в классе объявленные виртуальные функции (или он унаследован от класса содержащего виртуальные функции), приводит к тому, что класс будет дополнен указателем на таблицу виртуальных функций, и с этим указателем, таже проблема, что и со всеми другими. Опять же не POD-тип.
 +
*Если ваш класс содержит внутри себя не POD типы или унаследован от не POD-типа, то ваш класс тоже не под тип, т.е. нет никакой гарантии, что копирование куска памяти позволит постановить состояние класса.
 +
*Различное выравнивание данных внутри класса может сделать не возможным перенос сохранённого класса на другую платформу или даже в программу скомпилированную с другими параметрами компиляции.
 +
*Различный порядок байт не позволит переносить данные между такими платформами, как: x86 и PowerPC
 +
*И такая сериализация не даёт сохранить в удобочитаемой форме: XML, текст или CSV
 +
 
 +
Есть куча ситуация, когда создание дампа памяти вполне допустимый метод сериализации, но так же, есть другая куча ситуаций когда такой подход не допустим
  
 
Естественно выполняя сериализацию хочется, что бы она была максимально прозрачной и удобной.  
 
Естественно выполняя сериализацию хочется, что бы она была максимально прозрачной и удобной.  

Версия 08:04, 23 сентября 2008

Иногда нужно сохранить состояние класса в файл, передать состояние класса по сети. Подобные задачи обычно решает сериализация.

Говоря о сериализации, я подразумиваю механизмы перевода некоторого класса, структуры или набора переменных в определённый формат (бинарный, текстовый, 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);
	}
};

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