CRTP + compile-time introspection + static ctors = WIN

H. S. Teoh hsteoh at quickfur.ath.cx
Fri Jan 15 18:31:18 UTC 2021


Recently, I needed to extend a simple serialization system I wrote for
one of my projects to handle polymorphic objects.  It's all data-only
structs and classes, so no need for fancy heavyweight serialization
libraries.

One way to do this was to add load() and save() methods in the base
class, and override them for every derived class.  However, this would
be too much boilerplate, and prone to mistakes (forget to override a
method, and derived class data would fail to be serialized).

Another solution is to use mixins to inject these methods into each
derived class. But again, too much boilerplate, and prone to forgetting
to include the mixin statement in the class.

Yesterday, I hit upon a nice solution: use CRTP (the Curiously-Recursive
Template Pattern) to inject these methods into each derived class:

	class Saveable(Derived, Base) : Base {
		static if (is(Base == Object)) {
			// Top-level virtual function
			void save() { ... }
		} else {
			// Derived class override
			override void save() { ... }
		}
	}

	class Base : Saveable!(Base, Object) { ...  }

	class Derived1 : Saveable!(Derived1, Base) { ... }

	class Derived2 : Saveable!(Derived1, Base) { ... }

Since the CRTP is right at the first line of the class declaration, it's
hard to miss, and it's easy to notice when I forgot to include it (as
opposed to a mixin line buried somewhere in a potentially large class
definition).

The Base parameter to Saveable lets us nicely inject overridable methods
into the class hierarchy, and also to differentiate between top-level
methods and derived class overrides.

Saveable.save() uses the template argument to introspect the derived
class and generate code to serialize its fields. It includes code to
generate a tag in the serialized output to identify what type it is.

That takes care of the serialization half of the task.

For deserialization, there was the possibility of using Object.factory.
However, the API is klunky, and there is a disconnect with how to read
the fields back with the right types.

For this, static ctors come to the rescue. I expanded Saveable thus:

	alias Loader = Object function(InputFile);
	Loader[string] classLoaders;

	class Saveable(Derived, Base) : Base {
		static if (is(Base == Object)) {
			// Top-level virtual function
			void save() { ... }
		} else {
			// Derived class override
			override void save() { ... }
		}

		static this()
		{
			classLoaders[Derived.stringof] = (InputFile f) {
				auto result = new Derived;
				... // use introspection to read Derived's fields back
				return result;
			};
		}
	}

The magic here is that the static this() block is generated *once per
instantation* of Saveable, and it has full compile-time knowledge of the
derived class. So the function literal can use compile-time
introspection to generate the serialization code.  Then this knowledge
is translated to runtime by registering the function literal into a
global table of loaders, keyed by the class name. (For simplicity, I
used .stringof here; for larger-scale projects you probably want
.mangleof instead.)

And since static this() blocks are run at program startup and dynamic
library load time, this ensures that after program startup,
`classLoaders` has knowledge of all types the program will ever use.
So the deserialization code can simply look up the saved type tag in
`classLoaders`, and call the function pointer to reconstruct the object.

The result: to make any class serializable, you just replace:

	class MyClass : MyBase { ... }

with

	class MyClass : Saveable!(MyClass, MyBase) { ... }

and everything else is taken care of automatically. No need for mixins,
no need for repetitious serialization boilerplate polluting every class,
no need even for runtime TypeInfo's.  This will support even class
definitions loaded via dynamic libraries -- as long as you use
Runtime.loadLibrary to ensure static ctors are run -- since the static
ctors will inject any new class loaders into `classLoaders`, thus
automatically "teaching" the deserialization code how to deserialize the
corresponding classes.

CRTP + compile-time introspection + static ctors = WIN

D rocks!!


T

-- 
"Outlook not so good." That magic 8-ball knows everything! I'll ask about Exchange Server next. -- (Stolen from the net)


More information about the Digitalmars-d mailing list