How to get compatible symbol names and runtime typeid names for templated classes?

H. S. Teoh hsteoh at quickfur.ath.cx
Tue May 3 15:08:53 UTC 2022


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


More information about the Digitalmars-d-learn mailing list