On Tue, May 03, 2022 at 04:38:53PM +0200, Arafel via Digitalmars-d-learn wrote: > On 3/5/22 15:57, Adam D Ruppe wrote: > > So doing things yourself gives you some control. > > Yes, it is indeed possible (I acknowledged it), but I think it's much > more cumbersome than it should, and puts the load on the user. > > If templated this worked in static context (ideally everywhere else > too), then we'd be able to implement RTTI in a 100% "pay as you go" > way: just inherit from SerializableObject, or perhaps add a mixin to > your own root class, and that'd be it. > > Actually, it would be cool to do it through an interface, although I > don't think an interface's static constructors are invoked by the > implementing classes... it would be cool, though.
The way I did it in my own serialization code is to use CRTP with static ctors in templated wrapper structs. Namely, replace: class Base { ... } class Derived : Base { ... } with: class Base : Serializable!(Base) { ... } class Derived : Serializable!(Base, Derived) { ... } That's the only thing user code classes need to do. The rest is done in the Serializable proxy base class using compile-time introspection. In a nutshell, what Serializable does is to inject serialize() and deserialize() methods into the class hierarchy. Here's a brief sketch of what it looks like: class Serializable(Base, Derived = Base) : Base { static if (is(Base == Derived)) // this is the base of the hierarchy { // Base class declarations void serialize(...) { ... // use introspection to extract data members } void deserialize(...) { ... // use introspection to reconstitute data members } } else // this is a derived class in the hierarchy { override void serialize(...) { ... // use introspection to extract data members } override void deserialize(...) { ... // use introspection to reconstitute data members } } // How does the deserializer recreate an instance of // Derived? By registering the string name of the class // into a global hash: static struct Proxy // N.B.: this is instantiated for each Derived class { // This static this gets instantiated per // Derived class, and uses compile-time // knowledge about Derived to generate code for // reconstructing an instance of Derived. static this() { deserializers[Derived.stringof] = { auto obj = new Derived(); obj.deserialize(...); return obj; }; } } } // This is module-global. /*shared*/ static Object delegate(...)[string] deserializers; // Global deserialize method that returns an instance of the // class hierarchy. Object deserialize(...) { // Obtain class name from serialized data string classname = ...; // Dispatch to the correct method registered by Proxy's // static this, that recreates the class of the required // type. return deserializers[classname](...); } The nice thing about this approach is that you have full compile-time information about the target type `Derived`, in both the serialization and deserialization methods. So you can use introspection to automate away most of the boilerplate associated with serialization code. E.g., iterate over __traits(allMembers) to extract data fields, inspect UDAs that allow user classes to specify how the serialization should proceed, etc.. T -- Doubtless it is a good thing to have an open mind, but a truly open mind should be open at both ends, like the food-pipe, with the capacity for excretion as well as absorption. -- Northrop Frye