The answer by Yacoby can be extended further.
I believe the serialization can be implemented in a way similar to managed languages if one actually implements a reflection system.
For years we've been using the automated approach.
I was one of the implementors of the working C++ postprocessor and the Reflection library: LSDC tool and Linderdaum Engine Core (iObject + RTTI + Linker/Loader). See the source at http://www.linderdaum.com
The class factory abstracts the process of class instantiation.
To initialize specific members, you might add some intrusive RTTI and autogenerate the load/save procedures for them.
Suppose, you have the iObject class at the top of your hierarchy.
// Base class with intrusive RTTI
class iObject
{
public:
iMetaClass* FMetaClass;
};
///The iMetaClass stores the list of properties and provides the Construct() method:
// List of properties
class iMetaClass: public iObject
{
public:
virtual iObject* Construct() const = 0;
/// List of all the properties (excluding the ones from base class)
vector<iProperty*> FProperties;
/// Support the hierarchy
iMetaClass* FSuperClass;
/// Name of the class
string FName;
};
// The NativeMetaClass<T> template implements the Construct() method.
template <class T> class NativeMetaClass: public iMetaClass
{
public:
virtual iObject* Construct() const
{
iObject* Res = new T();
Res->FMetaClass = this;
return Res;
}
};
// mlNode is the representation of the markup language: xml, json or whatever else.
// The hierarchy might have come from the XML file or JSON or some custom script
class mlNode {
public:
string FName;
string FValue;
vector<mlNode*> FChildren;
};
class iProperty: public iObject {
public:
/// Load the property from internal tree representation
virtual void Load( iObject* TheObject, mlNode* Node ) const = 0;
/// Serialize the property to some internal representation
virtual mlNode* Save( iObject* TheObject ) const = 0;
};
/// function to save a single field
typedef mlNode* ( *SaveFunction_t )( iObject* Obj );
/// function to load a single field from mlNode
typedef void ( *LoadFunction_t )( mlNode* Node, iObject* Obj );
// The implementation for a scalar/iObject field
// The array-based property requires somewhat different implementation
// Load/Save functions are autogenerated by some tool.
class clFieldProperty : public iProperty {
public:
clFieldProperty() {}
virtual ~clFieldProperty() {}
/// Load single field of an object
virtual void Load( iObject* TheObject, mlNode* Node ) const {
FLoadFunction(TheObject, Node);
}
/// Save single field of an object
virtual mlNode* Save( iObject* TheObject, mlNode** Result ) const {
return FSaveFunction(TheObject);
}
public:
// these pointers are set in property registration code
LoadFunction_t FLoadFunction;
SaveFunction_t FSaveFunction;
};
// The Loader class stores the list of metaclasses
class Loader: public iObject {
public:
void RegisterMetaclass(iMetaClass* C) { FClasses[C->FName] = C; }
iObject* CreateByName(const string& ClassName) { return FClasses[ClassName]->Construct(); }
/// The implementation is an almost trivial iteration of all the properties
/// in the metaclass and calling the iProperty's Load/Save methods for each field
void LoadFromNode(mlNode* Source, iObject** Result);
/// Create the tree-based representation of the object
mlNode* Save(iObject* Source);
map<string, iMetaClass*> FClasses;
};
When you define the ConcreteClass derived from iObject, you use some extension and the code generator tool to produce the list of save/load procedures and the registration code for.
Let us see the code for this sample.
Somewhere in the framework we have an empty formal define
#define PROPERTY(...)
/// vec3 is a custom type with implementation omitted for brevity
/// ConcreteClass2 is also omitted
class ConcreteClass: public iObject {
public:
ConcreteClass(): FInt(10), FString("Default") {}
/// Inform the tool about our properties
PROPERTY(Name=Int, Type=int, FieldName=FInt)
/// We can also provide get/set accessors
PROPERTY(Name=Int, Type=vec3, Getter=GetPos, Setter=SetPos)
/// And the other field
PROPERTY(Name=Str, Type=string, FieldName=FString)
/// And the embedded object
PROPERTY(Name=Embedded, Type=ConcreteClass2, FieldName=FEmbedded)
/// public field
int FInt;
/// public field
string FString;
/// public embedded object
ConcreteClass2* FEmbedded;
/// Getter
vec3 GetPos() const { return FPos; }
/// Setter
void SetPos(const vec3& Pos) { FPos = Pos; }
private:
vec3 FPos;
};
The autogenerated registration code would be:
/// Call this to add everything to the linker
void Register_ConcreteClass(Linker* L) {
iMetaClass* C = new NativeMetaClass<ConcreteClass>();
C->FName = "ConcreteClass";
iProperty* P;
P = new FieldProperty();
P->FName = "Int";
P->FLoadFunction = &Load_ConcreteClass_FInt_Field;
P->FSaveFunction = &Save_ConcreteClass_FInt_Field;
C->FProperties.push_back(P);
... same for FString and GetPos/SetPos
C->FSuperClass = L->FClasses["iObject"];
L->RegisterClass(C);
}
// The autogenerated loaders (no error checking for brevity):
void Load_ConcreteClass_FInt_Field(iObject* Dest, mlNode* Val) {
dynamic_cast<ConcereteClass*>Object->FInt = Str2Int(Val->FValue);
}
mlNode* Save_ConcreteClass_FInt_Field(iObject* Dest, mlNode* Val) {
mlNode* Res = new mlNode();
Res->FValue = Int2Str( dynamic_cast<ConcereteClass*>Object->FInt );
return Res;
}
/// similar code for FString and GetPos/SetPos pair with obvious changes
Now, if you have the the JSON-like hierarchical script
Object("ConcreteClass") {
Int 50
Str 10
Pos 1.5 2.2 3.3
Embedded("ConcreteClass2") {
SomeProp Value
}
}
The Linker object would resolve all the classes and properties in Save/Load methods.
Sorry for the long post, the implementation grows even larger when all the error handling comes in.