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