Сериализация и С++11


Уверен, что многим кто работает с С++ хотелось, чтобы в этом, дивном языке, была возможность сериализовать объекты так же просто, как скажем в С#. Вот и мне этого захотелось. И я подумал, а почему бы и нет, с помощью нового стандарта это должно быть несложно. Для начала стоит определиться с тем, как это должно выглядеть.
class Test : public Serializable
{
public:
	int SomeInt = 666;
	float SomeFloat = 42.2; 
	string SomeString = "Hello My Little Pony";
private:
	serialize(SomeInt);
	serialize(SomeFloat);
	serialize(SomeString);
};

Такое мне вполне подходило, и я уже представлял себе решение.

У нас же есть C++11, а это в свою очередь означало, что у нас в распоряжении имеются лямбды и инициализация полей в объявлении класса. Соответственно можно писать подобные штуки.
struct Test
{
	string SomeString = "Hello My Little Pony";
	function<void()> SomeFunc = [this]()
	{
		cout << SomeString;
	};
};

Для начала напишем класс Serializable который хранил бы в себе все эти лямбды, и имел методы для сериализации и десериализации.
class Serializable
{
protected:
	typedef function<void(const string&)> Func;

	struct SerializerPair
	{
		Func Serializer;
		Func Deserializer;
	};

	char Add(string _key, Func _serializer, Func _deserializer)
	{
		auto& lv_Pair = m_Serializers[_key];
		lv_Pair.Serializer = _serializer;
		lv_Pair.Deserializer = _deserializer;
	}

public:
	virtual void Serialize()
	{
		for (auto& lv_Ser : m_Serializers)
			lv_Ser.second.Serializer(lv_Ser.first);
	}

	virtual void Deserialize()
	{
		for (auto& lv_Ser : m_Serializers)
			lv_Ser.second.Deserializer(lv_Ser.first);
	}

private:
	map<string, SerializerPair> m_Serializers;
};

Тут всё просто добавляем лямбды, и потом вызываем их.
Добавление выглядит так.
class TestClass : public Serializable
{
public:
	int SomeInt = 666;

private:
	char SomeIntSer = Add
	(
		"SomeInt",
		[this](const string& _key)
		{
			::Serialize(_key, SomeInt);
		},
		[this](const string& _key)
		{
			::Deserialize(_key, SomeInt);
		}
	);
};

Функции Serialize и Deserialize выносят саму логику сериализации за пределы класса, что позволяет нам легко расширять функционал.
Но это слишком избыточно, не так ли? На данном этапе к нам на помощь приходят макросы.
#define UNNAMED_IMPL(x, y) UNNAMED_##x##_##y
#define UNNAMED_DECL(x, y) UNNAMED_IMPL(x, y)
#define UNNAMED UNNAMED_DECL(__LINE__ , __COUNTER__)

// Макрос UNNAMED нам нужен для генерации не повторяющихся имён

#define serialize(x) char UNNAMED = Add		\
(											\
	#x,							  			 \
	[this](const string& _key)				  \
	{										   \
		::Serialize(_key, x);			\
	},											\
	[this](const string& _key) 	\
	{										\
		::Deserialize(_key, x);			\
	}										\
)

После этого наш предыдущий код выглядит уже гораздо меньше, и так как я хотел.
class TestClass : public Serializable
{
public:
	int SomeInt = 666;

private:
	serialize(SomeInt);
};

Всё бы хорошо, но мне кажется, что можно сделать ещё лучше. Если бы мы могли указывать контейнер для сериализации, то это дало бы нам +10 к удобству. Всё что нам нужно, так это сделать из Serializable шаблонный класс, которому мы могли бы сказать какой контейнер нужно прокидывать. Мужик сказал мужик сделал.
template<class Container>
class Serializable
{
protected:
	typedef function<void(const string&, Container&)> Func;

	struct SerializerPair
	{
		Func Serializer;
		Func Deserializer;
	};

	Container* ContainerInf = 0;

	char Add(string _key, Func _serializer, Func _deserializer)
	{
		auto& lv_Pair = m_Serializers[_key];
		lv_Pair.Serializer = _serializer;
		lv_Pair.Deserializer = _deserializer;

		return 0;
	}

public:
	virtual void Serialize(Container& _cont)
	{
		for (auto& lv_Ser : m_Serializers)
			lv_Ser.second.Serializer(lv_Ser.first, _cont);
	}

	virtual void Deserialize(Container& _cont)
	{
		for (auto& lv_Ser : m_Serializers)
			lv_Ser.second.Deserializer(lv_Ser.first, _cont);
	}

private:

	map<string, SerializerPair> m_Serializers;
};

Возможно вам интересно для чего нужен ContainerInf, а нужен он нам для того чтобы грамотно переделать наш макрос. Но для начала расширим возможности нашего сериализатора ещё чуть чуть. Сделаем наши глобальные функции Serialize и Deserialize шаблонными, чтобы не писать для каждого типа эти функции. Но тут появляется маленькая проблема. Шаблонная функция выполняется для того типа который мы ему дали, а потому не получится специализировать её так, чтобы она принимала отдельно объекты которые унаследованы от Serializable, а хочется ((. Для этого применим немножко шаблонной магии.
template<bool UNUSE>
struct SerializerEX
{};

template<>
struct SerializerEX < false >
{
	template<class T, class Cont, class UNUSE>
	void Serialize(const string& _key, T& _val, Cont& _cont, UNUSE)
	{
		::Serialize(_key, &_val, _cont);
	}

	template<class T, class Cont, class UNUSE>
	void Deserialize(const string& _key, T& _val, Cont& _cont, UNUSE)
	{
		::Deserialize(_key, &_val, _cont);
	}
};

template<>
struct SerializerEX < true >
{
	template<class T, class Cont, class UNUSE>
	void Serialize(const string& _key, T& _val, Cont& _cont, UNUSE)
	{
		::Serialize(_key, (UNUSE)&_val, _cont);
	}

	template<class T, class Cont, class UNUSE>
	void Deserialize(const string& _key, T& _val, Cont& _cont, UNUSE)
	{
		::Deserialize(_key, (UNUSE)&_val, _cont);
	}
};

Теперь мы можем смело переписать наш макрос.
#define serialize(x) char UNNAMED = Add																	\
(																										\
	#x,								\
	[this](const string& _key, ClearType<decltype(ContainerInf)>::Type& _cont)							\
	{																									\
		SerializerEX																					\
		<																								\
			CanCast																						\
			<																							\
				Serializable< ClearType<decltype(ContainerInf)>::Type >,								\
				ClearType<decltype(x)>::Type															\
			>::Result																					\
		> EX;																							\
		EX.Serialize(_key, x, _cont, (Serializable< ClearType<decltype(ContainerInf)>::Type >*)0);		\
	},																									\
	[this](const string& _key, ClearType<decltype(ContainerInf)>::Type& _cont) 					\
	{																									\
		SerializerEX																					\
		<																								\
			CanCast																						\
			<																							\
				Serializable< ClearType<decltype(ContainerInf)>::Type >,								\
				ClearType<decltype(x)>::Type															\
			>::Result																					\
		> EX;																							\
		EX.Deserialize(_key, x, _cont, (Serializable< ClearType<decltype(ContainerInf)>::Type >*)0);	\
	}																									\
)

Реализацию классов CanCast и ClearType я не буду описывать, они довольно тривиальные, в случае если «Ну очень надо» можно будет посмотреть их в исходниках прикреплённых к статье.
Ну и как же тут не показать пример использования. В роли контейнера я выбрал довольно известный Pugi XML
Пишем наши проверочные классы.
struct Float3
{
	float X = 0;
	float Y = 0;
	float Z = 0;
};

class Transform : public Serializable < pugi::xml_node >
{
public:
	Float3 Position;
	Float3 Rotation;
	Float3 Scale;

private:
	serialize(Position);
	serialize(Rotation);
	serialize(Scale);
};

class TestClass : public Serializable<pugi::xml_node>
{

public:
	int someInt = 0;
	float X = 0; 
	string ObjectName = "Test";
	Transform Transf;
	map<string, float> NamedPoints;

	TestClass()
	{
		NamedPoints["one"] = 1;
		NamedPoints["two"] = 2;
		NamedPoints["three"] = 3;
		NamedPoints["PI"] = 3.1415;
	}

private:
	serialize(X);
	serialize(ObjectName);
	serialize(Transf);
	serialize(NamedPoints);
};

Теперь проверка.
void Test()
{
	{
		TestClass lv_Test;
		lv_Test.ObjectName = "Hello my little pony";
		lv_Test.X = 666;
		lv_Test.Transf.Scale.X = 6;
		lv_Test.Transf.Scale.Y = 6;
		lv_Test.Transf.Scale.Z = 6;

		pugi::xml_document doc;
		auto lv_Node = doc.append_child("Serialization");
		lv_Test.Serialize(lv_Node);

		doc.save_file(L"Test.xml");
		doc.save(cout);
	}

	{
		pugi::xml_document doc;
		doc.load_file(L"Test.xml");
		auto lv_Node = doc.child("Serialization");

		TestClass lv_Test;
		lv_Test.Deserialize(lv_Node);

		cout << "Test passed : " << 
			(
				lv_Test.X == 666 && 
				lv_Test.ObjectName == "Hello my little pony" && 
				lv_Test.Transf.Scale.X && 
				lv_Test.Transf.Scale.Y && 
				lv_Test.Transf.Scale.Z
			);
	}
}

На выходе получаем
<?xml version="1.0"?>
<Serialization>
	<NamedPoints>
		<PI value="3.1415" />
		<one value="1" />
		<three value="3" />
		<two value="2" />
	</NamedPoints>
	<ObjectName value="Hello my little pony" />
	<Transf>
		<Position x="0" y="0" z="0" />
		<Rotation x="0" y="0" z="0" />
		<Scale x="6" y="6" z="6" />
	</Transf>
	<X value="666" />
</Serialization>
Test passed : 1

Ура! Всё получилось и работет как надо.
Для большей информации советую скачать исходники.
Исходники тут ---> www.dropbox.com/s/e089fgi3b1jswzf/Serialization.zip?dl=0
UPD.
Нашёл более оригинальное решение для большего контроля типов.
Вместо того чтобы использовать класс SerializerEX, достаточно было чуть-чуть похимичить с декларацией сериализаторов. Превратив их из
void Serialize(const string& _key, T* _val, xml_node & _node)

в
void Serialize(const string& _key, T* _val, xml_node & _node,...)

Тем самым указывая вместо ... указатель на тип мы сможем добиться большего контроля. Например
void Serialize(const string& _key, T* _val, xml_node & _node, Widget*)
{
...
}

будет работать только с объектами унаследованными от Widget* при этом сохраняя оригинальный тип.
Соответственно наш макрос при этом поменяется на более простой
#define serialize(x) char UNNAMED = Add																	\
(																										\
	#x,																									\
	[this](const string& _key, ClearType<decltype(ContainerInf)>::Type& _cont)							\
	{																									\
		::Serialize(_key, &x, _cont, (ClearType<decltype(x)>::Type*)0);									\
	},																									\
																										\
	[this](const string& _key, ClearType<decltype(ContainerInf)>::Type& _cont) 					\
	{																									\
		::Deserialize(_key, &x, _cont, (ClearType<decltype(x)>::Type*)0);								\
	}																									\
)
Поделиться публикацией
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 38

    +12
    Теперь мы можем смело переписать наш макрос.


    вы через месяц поймете, что там написано?
    Не используйте макросы там, где без них можно обойтись, отлаживать такой код очень затруднительно
      0
      Да могу, жаль хабр не сохраняет оригинальное форматирование. В коду всё выравнено. Да и цель была написать это для фана. Сомневаюсь что кто-то это будет использовать в реальном проекте.
        0
        У вас просто строки по длине не влезают, поэтому происходит перенос на следующую строку.
        +3
        отлаживать такой код очень затруднительно

        Что в данном случае подразумевается под «отладкой»? Пошаговая трассировка в отладчике — да, это может быть затруднительно, если макрос сплошь состоит из вызовов функций. А вот в плане отладки «во что это экспандится» макросы на много голов выше шаблонов: начиная от gcc -E и заканчивая отладкой макроэкспанда в gdb.
        P. S. только слепые рабы Саттера и Александреску боятся макросов, нормальные разработчики используют все доступные средства для повышения выразительности и читаемости кода.
          +2
          Просто там багов неявных можно наловить, начиная от классических примеров с забытыми скобками и использованном два раза параметром, заканчивая тем, что макросы уважают только запятые в круглых скобочках, но не в фигурных, угловых или квадратных:
          #define SIMPLE_MACRO(text) text
          
          SIMPLE_MACRO( vector<int> v(1, 2) ); // OK
          SIMPLE_MACRO( vector<int> v{1, 2} ); // Error, 2 arguments
          SIMPLE_MACRO( array<int, 10> ar ); // Error, 2 arguments
          SIMPLE_MACRO( [x, y](){} ); // Error, 2 arguments
          
          Слабенькие в плюсах макросы, жалко, что их не улучшают.
            +1
            Их надо уметь использовать, эта проблема легко решается:
            #define SIMPLE_MACRO(...) __VA_ARGS__
            А вот я недавно сделал себе макросы с дефолтными аргументами, это было чуть сложнее ))
              +1
              В таком тривиальном случае — да, но в случае, когда мы хотим с аргументами что-нибудь делать, не всё так просто. Особенно если такой случай заранее не продумать.
        +4
        Эм… Чем это решение лучше по сравнению с ::boost::serialization?
          +4
          Возможно тем, что не использует boost? хотя использует pugi, с другой стороны
            0
            И, насколько я понимаю, необходимо наследоваться от Serializable, что тоже не всегда хорошо.
            +1
            Вся соль была в синтаксисе. Упростить его так, что бы пользователю ненужно было писать свои функции для сериализации в каждом классе.
              +2
              Не могу удержаться и не перефразировать: «Вся соль была в сахаре. В синтаксическом сахаре.»
            +2
            Есть восможность сериализировать и, самое главное, десериализировать обьект производного класса полиморфно по указателю на базовый?
              0
              По указателю сериализовывать вообще нельзя но если очень хочется )) то можно поиграть с классом SerializerEX и добавить туда поддержку указателей. А сериализовывать и десериализовывать можно, если есть нужная функция для сериализации \ десериализации. В исходниках есть пример с полем унаследованным от Serializable что позволяет его сериализовать.
                0
                Это больноватое ограничение. К примеру, у вас есть вектор указателей на класс Widget. Что на самом деле за каждым из них скрывается известно лишь шефу Аллаха. И вот, Вам нужно все виджеты сохранить, а потом загрузить. У boost'а это достигается с помощью уродливых препроцессорных манипуляций. Любое другое решение было бы интересным.
                  0
                  Да тут это вроде из коробки работает, главное, чтобы во всей иерархии был только один Serializable объект и Widget был его потомком. Ах да, и ещё при текущей реализации надо, чтобы не было членов с одинаковыми именами, иначе в map один такой член затрет другого.
                    0
                    Как я понял он имеет ввиду работу с указателями.
                    +1
                    Ну в данном случае нужно просто определить функцию на подобии такой
                    template<class T>
                    void Serialize(const string& _key, vector<string, Widget*>* _widgets, pugi::xml_node& _node)
                    {
                    	auto lv_Node = _node.append_child(_key.c_str());
                    	int i = 0;
                    	for (auto lv_Widget : *_widgets)
                    	{
                    		lv_Widget->Serialize( lv_Node.append_child( inttostr(i++) ) );
                    	}
                    }
                    

                    Ну и соответственно для десериализации нечто подобное.
                      0
                      Вот это нечто подобное как раз и есть самое интересное. Ведь нужно вызвать оператор new с типом класса-наследника, а не с базовым.
                        0
                        Это уже вопрос фабрик. В функции сериализации можно указать что-то вроде
                        lv_Node.append_attribute("type") = Widget->Type();
                        

                        а в десериализации
                        _widgets[i] = CreateWidget( lv_Node.attribute("type").as_string() );
                        

                        В общем это уже выходит за пределы сериализатора, я для этого и вынес функции Serialize и Deserialize наружу, чтобы можно было легко расширять функционал.
                +6
                И под каждый объект создается целый набор функторов, хранящих указатель на this, и не являющихся не только copy-safe, но и move-safe, и это без удаленных соответствующих конструкторов и операторов. Я уже молчу про неинициализированные фиктивные члены класса…
                «Простой способ отстрелить всю ногу в C++. А почему бы и нет?»
                  0
                  и это без удаленных соответствующих конструкторов и операторов.

                  В исходниках все соответствующие конструкторы и операторы спрятаны, просто не стал писать в статью чтобы кода было не так много.
                  Я уже молчу про неинициализированные фиктивные члены класса

                  В смысле неинициализированные?
                    0
                    В исходниках все соответствующие конструкторы и операторы спрятаны, просто не стал писать в статью чтобы кода было не так много.

                    Куда спрятаны? У вас там default.

                    В смысле неинициализированные?

                    Извиняюсь, невнимательно прочитал код, в первом варианте у вас void Add(...), а во втором уже char.
                      0
                      Куда спрятаны? У вас там default.

                      Я понял прошу прощения, нужно было их просто пустыми оставить, это я почему то упустил.
                  +7
                  Действительно простая сериализация.
                  Скрытый текст
                    0
                    Имелось ввиду для использования конечно ))
                    +3
                    Какое-то у вас чрезмерно усложненное решение, на мой взгляд, для каждого экземпляра класса при создании выполняется дополнительный runtime код и тратится уйма памяти. При этом, насколько я понимаю, никакие плюсы подобного подхода (например, возможность в runtime для конкретного объекта поменять список сериализуемых элементов) вам не нужны, вся нужная информацию у вас есть в compile-time и единственное преимущество по сравнению с каким-нибудь банальным решением типа:
                    class Test
                    {
                    public:
                        int SomeInt = 666;
                        float SomeFloat = 42.2; 
                        string SomeString = "Hello My Little Pony";
                    
                        void Serialize() {
                            ::Serialize("SomeInt", SomeInt);
                            ::Serialize("SomeFloat", SomeFloat);
                            ::Serialize("SomeString", SomeString);
                        }
                    };
                    

                    лишь красивый синтаксис и отсутствие повторов. Если я прав, то подумайте над каким-нибудь таким решением: для каждого класса храним статическую переменную-tuple, в которой лежат имена и указатели на члены данных (pointer-to-member), что-нибудь вида:
                    class Test
                    {
                    public:
                        int SomeInt = 666;
                        float SomeFloat = 42.2; 
                        string SomeString = "Hello My Little Pony";
                    private:
                        static constexpr std::tuple<...> serialization_info { &Test::SomeInt, "SomeInt",
                                    &Test::SomeFloat, "SomeFloat",
                                    &Test::SomeString, "SomeString" };
                    };
                    

                    Ну и, соотвественно, функциям сериализации вы просто передаете указатель на объект и ссылку на данный tuple, они по нему итерируются и делают всё, что нужно. Думаю, поплясав с бубном, можно всё это красиво обернуть и даже от этой статической переменной избавиться. Если интересно, можете ещё поглядеть недавнюю статью, она тяжеловато написана, но там похожая конструкция используется.
                      –1
                      для каждого экземпляра класса при создании выполняется дополнительный runtime код и тратится уйма памяти.

                      К сожалению да, не зря есть фраза что за удобство нужно платить.
                      лишь красивый синтаксис и отсутствие повторов.

                      Да в этом и был смысл ))
                      то подумайте над каким-нибудь таким решением: для каждого класса храним статическую переменную-tuple, в которой лежат имена и указатели на члены данных (pointer-to-member), что-нибудь вида:

                      Да можно попробовать сделать та, но это было бы не так красиво как мне кажется.
                        +1
                        К сожалению да, не зря есть фраза что за удобство нужно платить.
                        Имхо, это не C++ way.
                          0
                          Согласен, да и цель была просто попробовать реализовать фичу с подобным синтаксисом, получилось, но сомневаюсь что буду это использовать когда-нибудь в реальном проекте.
                      +2
                      Как насчет msgpack, protobuf, cereal?
                        0
                        Не работал с msgpack, cereal, но по первому взгляду них всё тоже самое cereal не особо отличается от бустовской версии, а msgpack выглядит страшновато. Для protobuf'a нужно вообще писать протофайл формата. И я не ставил целью заменить их, сам активно использую протобаф, хотелось просто реализовать идею. Для несложных проектов возможно буду использовать это решения, для сложных скорее всего останусь на проверенном временем протобафе. Но спасибо за полезные ссылки сохранил себе.
                        0
                        Прочел бегло.
                        Как я понимаю, к каждому объекту, который мы хотим сериализовать, мы должны добавить соответствующие методы?

                        А в плюсах есть статическая информация о классе, которую можно получить в runtime? Например, получить список public членов и пройтись по ним, с автоматической сериализацией, в зависимости от типа? У нас в команде в дельфях так и сделали — проходятся все published property и они сериализуются в специальный контейнер, выдающий строку на выходе. Можно ли сделать так же?
                          +1
                          Нет, интроспекция (compile-time) есть только на уровне proposal.
                            +1
                            Как я понимаю, к каждому объекту, который мы хотим сериализовать, мы должны добавить соответствующие методы?

                            В данном случае эти методы прописываются макросами, самому ручами ничего писать не надо.
                            А в плюсах есть статическая информация о классе, которую можно получить в runtime?

                            Нет, в С++ нет рефлекшена. Потому и приходится писать подобные штуки.
                              0
                              Возможно, когда-нибудь будет.
                            +1
                            Дочитал до слова virtual, а дальше не смог.
                              +1
                              Вот здесь небольшое сравнение различных сериализаторов для C++: Benchmark comparing various data serialization libraries

                              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                              Самое читаемое