Pull to refresh

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

Reading time 6 min
Views 47K

Уверен, что многим кто работает с С++ хотелось, чтобы в этом, дивном языке, была возможность сериализовать объекты так же просто, как скажем в С#. Вот и мне этого захотелось. И я подумал, а почему бы и нет, с помощью нового стандарта это должно быть несложно. Для начала стоит определиться с тем, как это должно выглядеть.
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);								\
	}																									\
)
Tags:
Hubs:
+17
Comments 38
Comments Comments 38

Articles