Class-side inheritance (virtual static methods)


#1

Coming originally from Smalltalk, I sorely missed a lot of the expressiveness of that language in C++ (e.g. closures with non-local returns, symbols, class-side inheritance, reflection), some features of which however I managed to emulate or substitute. I want to share one particular simple solution with you, in the hope it will be useful:

Class-side Inheritance

C++ doesn’t natively support virtual static methods, that is, inheritance with overrides on the class side. Inheritance requires instances (vtables) and therefore classes would need to be instances of a meta class, which is not how C++ is implemented (the C++20 metaclasses proposal doesn’t solve this either, unfortunately).

It is quite possible that you don’t miss it yet, if you never used it.

Polymorphic class-side code is useful for all sorts of representing knowledge (constants, properties, behaviour, defaults) alongside the class hierarchy. Basically all code that can or must run without having an instance available goes there (e.g. policies, factories, instance registries, creating objects read from a stream, etc). Another typical use is a class-side ‘registry’ that manages instances of that class, e.g. for lookup or caching. Regular static methods can only do so much, as they can’t make use of inheritance. Once you get used to it, it greatly streamlines the organization of your code, in that everything relevant for a class is coded immediately there.

Implementation

The example shows two classes Cluster and Chord, plus there meta classes Cluster::Class and Chord::Class. Essentially we get a meta class hierarchy running in parallel to the class hierarchy. The meta classes implement their counterpart’s class-side methods. A singleton of each is held to run them and provide inheritance.

class Cluster
{
public:
	struct Class
	{
		virtual ~Class() {}
		static Class& methods() 
			{ static Class meta; return meta; }

		// All virtual class methods of Cluster go here
		virtual const String xmlTag() 
			{ return "cluster-structure"; }

		virtual Cluster readFromFile (InputStream s)
			{ ... }
	};
	virtual Class& class_methods() 
		{ return Class::methods(); }
}

class Chord : public Cluster
{
public:
	struct Class : public Cluster::Class
	{
		virtual ~Class() {}
        	static Class& methods() 
				{ static Class meta; return meta; }

		// All virtual class methods of Chord go here
		const String xmlTag() override 
			{ return "chord-structure"; }

		Chord readFromFile override (InputStream s)
		{
			auto c = Cluster::Class::methods().readFromFile(s);
			return whatever(c);
		}
	};
	Class& class_methods() override 
		{ return Class::methods(); }
}

The pattern is very repetitive and simple. It can be much condensed and made more legible with macros: (they should be trivial, but I can post them if needed)

class Cluster
{
public:
	CLASS_METHODS_ROOT

	virtual const String xmlTag() 
		{ return "cluster-structure"; }

	virtual Cluster readFromFile (InputStream s)
		{ ... }

	CLASS_METHODS_ROOT_END
}

class Chord : public Cluster
{
public:
	CLASS_METHODS_WITH_SUPERCLASS (Cluster)

	const String xmlTag() override 
		{ return "chord-structure"; }

	Chord readFromFile override (InputStream s)
	{
		auto c = Cluster::Class::methods().readFromFile(s);
		return whatever(c);
	}
	CLASS_METHODS_END
}

That’s basically all it takes. In your cpp file, you would implement (meta) class methods by adding ::Class to the class name, which makes those pretty self-explanatory:

Array<Chord*> Chord::Class::allInstances () {...}

Note that instance methods in Cluster that call class_methods().xmlTag() properly deliver 'chord-structure' if the instance currently running the code of Cluster is in fact a Chord (virtual class methods).

The primitive singleton ‘meta’ is probably not thread-safe. Juce singletons work too, albeit more verbose and not header-only.

Possibly this can be done with templates better than with macros? If so, I’d love to hear about it.


#2

Did you have a look at the “Curiously recurring template pattern”? It looks like a good candidate for what you are trying to achieve.


#3

as a TL;DR for CRTP, basically your base class provides the implementation and calls derived class member functions inside the implementations.

template<typename Derived>
struct Base
{
    void function() 
    { 
        Derived* d = dynamic_cast<Derived*>(this); 
        d->action(); //call the member function of the template parameter type
    }
    friend class Derived;
};

struct Derived : public Base<Derived>
{
    void action() { }
};

#4

Thanks. Yes, I tried CRTP, but it didn’t work. It ended up with ambiguous and redundant base classes in the meta class hierarchy. For polymorphism at the class-side to work, each local metaclass *::Class must inherit exclusively from its respective superclass’ metaclass (and all class-side methods must be virtual, of course). The only exception being the top-most root, which may inherit from some generic Metaclass, or something.

That’s what I just did to mitigate the thread-safety issue. Putting a class Metaclass at the root of the metaclass hierarchy ensures that a shared lock is initialised once and then used by all metaclasses to protect access to their singletons:

/**
 All meta classes inherit from Metaclass, which is the
 root of the parallel meta class hierarchy.
 */
class Metaclass
{
public:
    virtual ~Metaclass() {}
    static CriticalSection& sharedLock()
    {
        static CriticalSection lock;
        return lock;
    }
};

/**
 Starts the definition of the top-most meta class in your hierarchy, which implements
 class-side virtual methods of the class being defined in the current scope.
 */
#define CLASS_METHODS_ROOT \
    struct Class : public Metaclass \
    { static Class& methods() { const ScopedLock sl (Metaclass::sharedLock()); static Class meta; return meta; }

/** Ends definition of the top-most meta class */
#define CLASS_METHODS_ROOT_END \
    }; \
    virtual Class& class_methods() { return Class::methods(); }

/**
 Starts the definition of a meta class that inherits from S::Class
 Argument S is the immediate superclass of the class being defined in the current scope.
 */
#define CLASS_METHODS_WITH_SUPERCLASS(S) \
    struct Class : public S::Class \
    { static Class& methods() { const ScopedLock sl (Metaclass::sharedLock()); static Class meta; return meta; }

/** Ends the definition of a derived meta class */
#define CLASS_METHODS_END \
    }; \
    Class& class_methods() override { return Class::methods(); }

This still works w/o additional per-class code in a cpp file. If anything, one could enhance Metaclass and initialise its globals in a cpp file once. The goal is to avoid per-class initialisation in cpp files, as that would eliminate all convenience.

What I find so attractive is the ability to declare class-side virtual methods right inside the class definition. It looks clean and concise and just works.