📜 ⬆️ ⬇️

Serialization and C ++ 11


I am sure that many who work with C ++ would like to have the opportunity to serialize objects in this wondrous language as easy as we can say in C #. So I wanted it. And I thought, why not, with the help of the new standard it should be easy. First you need to decide on how it should look.
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); }; 

This was quite suitable for me, and I already imagined a solution.

We also have C ++ 11, and this in turn means that we have lambda and field initialization in the class declaration. Accordingly, you can write such things.
 struct Test { string SomeString = "Hello My Little Pony"; function<void()> SomeFunc = [this]() { cout << SomeString; }; }; 

To begin with, we will write a class Serializable which would store in itself all these lambdas, and had methods for serialization and deserialization.
 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; }; 

It's all just add lambda, and then call them.
Adding looks like this.
 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); } ); }; 

The functions Serialize and Deserialize take the very logic of serialization outside the class, which allows us to easily extend the functionality.
But this is too redundant, isn't it? At this stage, macros come to our rescue.
 #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); \ } \ ) 

After that, our previous code looks much smaller, and as I wanted.
 class TestClass : public Serializable { public: int SomeInt = 666; private: serialize(SomeInt); }; 

Everything is good, but it seems to me that you can do even better. If we could specify a container for serialization, this would give us +10 to convenience. All we need is to make a Serializable template class, which we can tell which container to throw. The guy said the guy did.
 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; }; 

Perhaps you are interested in what ContainerInf is for, but we need it in order to competently redo our macro. But first, let's expand the capabilities of our serializer a little more. Let's make our global functions Serialize and Deserialize template so as not to write these functions for each type. But then there is a small problem. The template function is performed for the type that we gave it, and therefore it will not be possible to specialize it so that it accepts objects that are inherited from Serializable separately, but want to ((. For this, apply a little template magic.
 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); } }; 

Now we can safely rewrite our macro.
 #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); \ } \ ) 

I will not describe the implementation of the CanCast and ClearType classes, they are quite trivial, if “Well, I really need it,” you can see them in the source code attached to the article.
Well, how can you not show an example of use. In the role of a container, I chose a fairly well-known Pugi XML
We write our test classes.
 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); }; 

Now check.
 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 ); } } 

At the output we get
 <?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 

Hooray! Everything turned out and work as it should.
For more information, I advise you to download the source.
Sources here ---> www.dropbox.com/s/e089fgi3b1jswzf/Serialization.zip?dl=0
UPD.
Found a more original solution for more type control.
Instead of using the SerializerEX class, it was enough to add a little bit to the declaration of serializers. Turning them out
 void Serialize(const string& _key, T* _val, xml_node & _node) 

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

By pointing instead of ... a pointer to the type, we can achieve greater control. for example
 void Serialize(const string& _key, T* _val, xml_node & _node, Widget*) { ... } 

will only work with objects inherited from Widget * while maintaining the original type.
Accordingly, our macro will change to a simpler one.
 #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); \ } \ ) 

')

Source: https://habr.com/ru/post/244963/


All Articles