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