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

Reply via email to