Open davjs opened 8 years ago
I like the idea of this as an optional way of using cereal. We can't have boost be a requirement however, as cereal needs to be self contained.
If can't be shed as a requirement, this could potentially fit in better with the idea of 'modules' for cereal (see the next milestone) which would be self-contained additions to the library.
Annoying cases users might eventually request of a feature like this:
Something interesting and semi-related (just considering the syntax you've got proposed matches identically), Boost::Hana introduces the ability to define structs with metadata in a similar way: http://www.boost.org/doc/libs/1_61_0/libs/hana/doc/html/index.html
There's a fantastic talk as well, I highly suggest watching it: https://channel9.msdn.com/Events/CPP/CppCon-2015/CPPConD04V003
I'm not sure if it's super applicable, but if you're going to introduce a macro anyway, it might be nice to look at what BOOST_HANA_DEFINE_STRUCT does and consider exposing your members to a reflection interface instead of just a bare call to archive which is way less useful imho.
// 1. Give introspection capabilities to 'Person'
struct Person {
BOOST_HANA_DEFINE_STRUCT(Person,
(std::string, name),
(int, age)
);
};
// 2. Write a generic serializer (bear with std::ostream for the example)
auto serialize = [](std::ostream& os, auto const& object) {
hana::for_each(hana::members(object), [&](auto member) {
os << member << std::endl;
});
};
// 3. Use it
Person john{"John", 30};
serialize(std::cout, john);
// output:
// John
// 30
As it stands, I'm happily using cereal because it's very flexible and I use load_and_construct and some other customizations which let me add/extract information from an archive to reconstitute things in their context. I couldn't benefit from such a macro except in the most trivial cases of my "Point" class (x, y, z) and a few other tiny edge cases... But perhaps I could benefit from added reflection which handles serialization, but which also allows me to hook up something like chaiscript.
Right now I have a bunch of code that looks like this:
class TexturePoint{
public:
TexturePoint(PointPrecision a_textureX, PointPrecision a_textureY):textureX(a_textureX), textureY(a_textureY){}
TexturePoint():textureX(0.0),textureY(0.0){}
~TexturePoint(){}
TexturePoint& operator=(const TexturePoint& a_other);
TexturePoint& operator=(const DrawPoint& a_other);
bool operator==(const TexturePoint& a_other) const{
return equals(textureX, a_other.textureX) && equals(textureY, a_other.textureY);
}
bool operator!=(const TexturePoint& a_other) const{
return !(*this == a_other);
}
template <class Archive>
void serialize(Archive & archive){
archive(CEREAL_NVP(textureX), CEREAL_NVP(textureY));
}
PointPrecision textureX, textureY;
static chaiscript::ChaiScript& hook(chaiscript::ChaiScript &a_script) {
a_script.add(chaiscript::user_type<TexturePoint>(), "TexturePoint");
a_script.add(chaiscript::constructor<TexturePoint()>(), "TexturePoint");
a_script.add(chaiscript::constructor<TexturePoint(PointPrecision, PointPrecision)>(), "TexturePoint");
a_script.add(chaiscript::fun(&TexturePoint::textureX), "textureX");
a_script.add(chaiscript::fun(&TexturePoint::textureY), "textureY");
a_script.add(chaiscript::fun(&TexturePoint::operator==), "==");
a_script.add(chaiscript::fun(&TexturePoint::operator!=), "!=");
return a_script;
}
};
Note the base definition of: textureX, textureY and then the chaiscript hook binding textureX, textureY, and finally cereal archive(textureX, textureY). While no bit of code in this is complex, this is the simplest example of a serializable and scriptable object that exists in my code. I made the decision to make everything in my scene graph save-able, load-able, and script-able. I've thought about how reflection could vastly help, but it really isn't quite there in C++ yet. I just saw the hana video though and am pretty impressed.
I think i have a better solution for this. I have the following macro
#include <Serialization/cereal/cereal.hpp>
#define SERIALIZABLE(T) \
public:\
friend class cereal::access;\
template<typename Archive> friend void CEREAL_SAVE_FUNCTION_NAME(Archive & ar, T const &);\
template<typename Archive> friend void CEREAL_LOAD_FUNCTION_NAME(Archive & ar, T &);
usage is the following: In your normal header
class MyClass
{
SERIALIZABLE(MyClass)
...
};
then in a COMPLETELY seperate file that you can change without the need to recompile the original class header every time you need to change something, thus saving a lot of compile time and achieving non extrusive serialization. You also dont't break the existing interface of the class this way
template<typename Archive> inline \
void CEREAL_SAVE_FUNCTION_NAME(Archive & ar, TransformComponent const & obj)
{
ar(
cereal::make_nvp("base_type", cereal::base_class<Component>(&obj)),
cereal::make_nvp("hierarchy_level", obj.mHierarchyLevel),
cereal::make_nvp("local_transform", obj.mLocalTransform),
cereal::make_nvp("children", obj.mChildren)
);
}
template<typename Archive> inline \
void CEREAL_LOAD_FUNCTION_NAME(Archive & ar, TransformComponent & obj)
{
ar(
cereal::make_nvp("base_type", cereal::base_class<Component>(&obj)),
cereal::make_nvp("hierarchy_level", obj.mHierarchyLevel),
cereal::make_nvp("local_transform", obj.mLocalTransform),
cereal::make_nvp("children", obj.mChildren)
);
}
#include <Serialization/Archives.hpp>
CEREAL_REGISTER_TYPE(TransformComponent);
Manually written serialization code is error prone because it requires replicating information. Especially when the goal is to serialize every member of a struct/class. Whenever the members are changed the serialization method(s) need to as well. More modern languages has solutions for this using reflection. C# uses a Serializable attribute, Java let's you extend/implement a XSerializable and dynamic languages usually work out of the box.
I started looking into the possibilities of simulating reflection in C++ and stumbled over this SO answer. http://stackoverflow.com/questions/11031062/c-preprocessor-avoid-code-repetition-of-member-variable-list/11744832#11744832 It's a macro designed to look like member variable declarations that generates a "reflection table" in addition to the members. After a while i realized we could do something similar but much more lightweight. A macro that just generates the serialization function in place.
The following:
Would then expand to:
Benefits with this approach
Its optional.
You don't have to use it if you don't want to. It's also a modular addition that doesn't require any overhaul of the underlying framework.
All members do not need to be serialized.
Other languages depend on access modifiers or individual attributes to decide whether a member is serialized or not. This macro simply serializes everything inside the macro and doesn't serialize members outside the macro. Much more explicit IMO.
Downsides
All members that are serialized need to have the same access modifier.
It is possible to add access modifiers before and after the macro but not to squeece in one in the middle of the macro. I personally don't think this is a huge issue, i think the most common use case for the macro is when all members are public.
The implementation can be found in here: https://github.com/davidkron/cereal/blob/master/include/cereal/serializable.hpp