Juliean said:
I assume Load in your system is virtual in CSearchable, correct?
Not only - there's a bit more going on behind the scenes. Consider this (I've nipped most of the actual code out for clarity and omitted LoadProperties() and OnLoad()):
// provide private proxies that save/load everything in _this_ class
#define CSEARCHABLE(...) \
... gore omitted ...\
private:\
ECBool SaveProperties(\
_In_Modify ::markup::CTagFile& tagFile,\
_Optional_In size_t rootId = size_t(~0)) const _Override { \
return CSearchable::SaveProperties(tagFile, rootId); \
}
class CSearchable
{
private:
ECBool SaveProperties(
_In_Modify ::markup::CTagFile& tagFile,
_Optional_In size_t rootId = size_t(~0)) const {
// magic happens here: save attribs, properties and call Children>>SaveProperties().
// Then invoke this class's top level handler:
EHCheck(OnSave(*tf, idThis));
return ECSuccess;
}
public:
// provide optional save/load handlers, but make them optional
_Basemethod
ECBool OnSave(
_In ::markup::CTagFile& tfProperties,
_In uint32_t idParent) const {
_Unref(tfProperties, idParent);
return ECSuccess;
}
};
For complex cases this might assume that the top level OnSave()/OnLoad() propagates the call if the top level class doesn't deal with its base class' custom stuff. Ideally you don't store custom stuff here at all, but everything is handle automatically.
Nevertheless, as it happens, there's a better way of doing this, which I'm using for my CSerialized class (again, routines related to deserialization and most of the gory details are omitted):
#define CSERIALIZABLE(ThisType) \
... gore ... \
private:\
template<bool Flag>\
typename ::std::enable_if<Flag == true, ECBool >::\
type SerializableOnSerializeHelper(\
CSerializedWriter& writer) const {\
return OnSerialize(writer);\
}\
template<bool Flag>\
typename ::std::enable_if<Flag == false, ECBool >::\
type SerializableOnSerializeHelper(\
CSerializedWriter& writer) const {\
_Unref(writer);\
return ECSuccess;\
}\
ECBool Serialize(\
CSerializedWriter& writer) const _Final {\
return SerializableOnSerialize(writer);\
}\
public:\
ECBool SerializableOnSerialize(\
CSerializedWriter& writer) const _Override {\
EHCheck(_ThisType::SerializableOnSerializeHelper<::muon::has_serializer_funcs<_ThisType>::value>(writer));\
... serialize automatic vars here ... \
if constexpr(has_super_v<_ThisType>)\
{ return has_super<_ThisType>::_SuperType::SerializableOnSerialize(writer); }\
else\
return ECSuccess;\
}
class _InterfaceClass ISerialized {
public:
// force the derived class to have a valid CSERIALIZABLE() as these are implmented
// by that macro
_PureBasemethod
ECBool Serialize(
CSerializedWriter& writer) const = 0;
_PureBasemethod
ECBool Deserialize(
const CSerializedReader& reader) = 0;
};
class CSerialized // <- NOT derived from ISerialized in order to better support multiple inheritance
{
protected:
// these functions are implemented by the CSERIALIZED() macro for each class
_PureBasemethod
ECBool SerializableOnSerialize(
CSerializedWriter& writer) const = 0;
_PureBasemethod
ECBool SerializableOnDeserialize(
const CSerializedReader& reader) = 0;
public:
// these functions may be override by each level of the inheritance tree to save/load custom data
_Basemethod
ECBool OnSerialize(
CSerializedWriter& writer) const {
_Unref(writer);
return ECSuccess;
}
_Basemethod
ECBool OnDeserialize(
const CSerializedReader& reader) {
_Unref(reader);
return ECSuccess;
}
};
Note that these are snippets of an unfinished framework, which hasn't been properly tested so there may be some oddities here and there.
In this scheme a class' “registered” variables (attribs, properties, etc) are automatically serialized. If the class contains some complex custom member objects, however, it can override to OnSerialize()/OnDeserialize() and handle them there. Where it becomes more intuitive than the scheme used for CSearchable is if different classes in one heritance tree need to serialize different “local” objects. This solution ensures that you cannot forget to serialize the base class(es). Consider:
class MyBase
: public CSerialized, public ISerialized // <- need to derive from both in this case (see previous snippet)
{
public:
CSERIALIZABLE(MyBase); // <- ugly bits are hidden here
MyDataStore* store; // some complex object that cannot be trivially serialized
ECBool OnSerialize(CSerializedWriter& writer) const _Override {
EHCheck(Helper_SerializeComplexStore(writer, *store)); // <- some magic black box
return ECSuccess;
}
...
};
class MyDerived
: public MyBase
{
public:
CSERIALIZABLE(MyDerived);
MyComplexCatModel scottishFold; // another complext object; in this case a _very_ complicated cat
ECBool OnSerialize(CSerializedWriter& writer) const _Override {
EHCheck(Helper_SerializeComplexCat(writer, scottishFold));
// NOTE: not propagating the call to the base class!
return ECSuccess;
}
}
Here OnSerialize() is called for all levels in the hierarchy without any attention from the programmer, starting with MyDerived (note that - if there were another class between MyBase and MyDerived, which did not need to be serialized, it would be skipped). It's invoked simply as:
MyDerived myderived;
CSerializedWriter writer;
myderived.Serialize(writer);
// 1. MyDerived::Serialize() calls MyDerived::SerializableOnSerialize()
// 2.1 this chooses whether to invoke MyDerived::OnSerialize() if both OnSerialize() AND OnDeserialize() are implemented (note that if either is present, then it's not valid and therefore becomes impossible to not to implement the other)
// 2.2 then serializes all automatic variables for MyDerived
// 2.3 finally, if MyDerived has a super class (I'm keeping track of class hierarchy separately - all of that code is omitted, but it becomes easy once you already have a macro which takes the this class' name), calls:
// _SuperType::SerializableOnSerialize(), which in this case happens to be MyBase::SerializableOnSerialize()
// we're back in point 2.1 now, so rinse and repeat until the hierarchy has been walked
Basically this implements a hidden top-down call tree, which checks if a given class implements both of the OnXXX() functions and calls them if needed. This ensures that things are always serialized and deserialized in the same order, that all objects are both written and read, provides an automatic framework for 90-100% of the writes/reads and overall minimizes a potential for mistakes by a huge margin.
As for serialization itself - CSerializedWriter
and its cousin CSerializedReader
can be made implementation-independent - the output COULD be binary, text, YAML, markup or whatever you want. In my case I've separated XML/JSON and other markup styles into the CSearchable and binary data into CSerialized. This just made sense to me as I won't want to expose binary objects (e.g. everything) to searching or an editor, but I do want to store attributes and properties in human-readable format in the form of settings. Also, this way I can parallelize stuff more easily.
My main objective is eventual simplicity hidden behind a (fairly complex) wrapper and, more importantly, reliability. If at all possible, I don't want to worry about saving and loading data beyond indicating “this needs to be saved”.
Juliean said:
I see pros and cons to both approaches, for example I can write, use and replace different forms of serialization (YAML, XML, binary) without having to touch the class itself.
The bottom line is: there's really no way of not touching the class itself. Either you end up over-complexifying your I/O routines to handle arbitrary classes (and sooner or later this will fall apart) or you need to provide (possibly extensive) metadata about serialized objects. Either way there's meddling involved. A while back I tried going with a handler-based system (e.g. each type has a type handler and serialized fields are exposed externally), but ended up in a handler hell. Plus, I kept forgetting to add attribs to the registrar. This is why I prefer my metadata to be clear and confined within the class itself: for instance, if a field is a non-dependent (e.g. not derived or dependent on the session state), then it can be an attribute. My objective is to make it as easy as possible to indicate that fact that something is an attribute without getting into external tools or dealing with the possibility of forgetting something somewhere.
By the way - I'm on C++17 and haven't looked into concepts at all. If you do end up skimming through the code, let me know if concepts might provide an easier solution for any of this?
Anyways - sorry for the long post ?.