However this will be hard to maintain as I add, remove and modify nodes and their loaders over the course of development (this is not a loader for some well defined standard which is not going to change).
Is there another way to do this? Can I get my compiler to automatically create a list of all classes that extend NodeLoader somewhere? |
"Automatic" may be a stretch to some degree, but you may have some alternative which is relatively similar.
If XmlReader were to be re-used in other software, the "init" function, as written, would be a burden upon the user to create a new, custom "init" for each application. An undesirable user requirement beyond the mere inconvenience your post states.
In order to compile your code example I had to supply a few "fake" items (I'm not building with Qt, for example), but something like the following might do what you like.
First, I propose a kind of "factory"
1 2 3 4 5 6 7 8 9 10 11
|
struct FactoryBase
{
virtual ~FactoryBase() {}
};
template < typename C >
struct NodeFactory : public FactoryBase
{
NodeFactory();
};
| |
Next, I propose a type of lazy initialization, but not of the loaderMap, of a factory map
1 2 3 4 5 6 7 8 9 10 11
|
struct FactoryBase;
typedef QMap< const QString, FactoryBase * > FactoryMap;
class XmlReader
{
public:
inline static FactoryMap * factoryMap { nullptr }; // C++ 17
};
| |
This does not replace your loaderMap, that remains as before, I'm merely illustrating the proposal components here.
The constructor for NodeFactory is something like:
1 2 3 4 5 6 7 8 9
|
template < typename C >
NodeFactory<C>::NodeFactory()
{
if ( !XmlReader::factoryMap ) XmlReader::factoryMap = new FactoryMap();
(*XmlReader::factoryMap)[ C::ID ] = this;
}
| |
This demonstrates lazy initialization of the factoryMap static, held via pointer. This is because during initialization of "global" instantiations, there is no guarantee of the order of construction (easily).
For each class, I then propose:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
|
class RectLoader : public NodeLoader
{
public:
inline static const QString ID { "rect" }; // C++ 17, and does this really need to be private?
public:
RectLoader() : NodeLoader(ID) {}
Node* load(QDomElement& ele) override {
RectNode* r = new RectNode();
//...
return r;
}
};
NodeFactory< RectLoader > RectFactory;
class TextLoader : public NodeLoader
{
public:
inline static const QString ID { "text" }; // C++ 17
public:
TextLoader() : NodeLoader(ID) {}
Node* load(QDomElement& ele) override {
TextNode* r = new TextNode();
//...
return r;
}
};
NodeFactory< TextLoader > TextFactory;
| |
This gives you the freedom to implement new classes with abandon, merely instantiating (by otherwise standard means) little factories which "register themselves" with the factoryMap static.
Now, as "globals", this is inconvenient for inclusion in the header, but "standard" means (extern declarations) this may be solvable. If the user creates this in a chosen translation unit, at least the user does not have to modify the XmlReader "init" function to use it in various application deployments.
This is, obviously, incomplete, but to a relatively capable C++ developer it should now be obvious that in the constructor for an XmlReader, the factoryMap could be used to call function(s) in the virtual base "FactoryBase" (not demonstrated here) to instantiate the appropriate entries from a loop in the XmlReader's initialization.
I'd use a callback approach, such that FactoryBase might have something like:
1 2 3 4 5 6
|
struct FactoryBase
{
virtual ~FactorBase(){}
virtual bool MakeNodeLoader( XmlReader * ) = 0;
};
| |
The point being that the derived "MakeNodeLoader" would now have the instance of XmlReader to "call back to XmlReader" for the submission of a new, appropriate loader.
This is obviously pseudo-code, example-ware, untested, conceptual code snippet stuff. I did, however, compile the basic notion and saw that it correctly created a factoryMap for XmlReader containing a "rect" and a "text" NodeFactory, which is to say the derivatives would have full knowledge by which to instantiate RectLoader, TextLoader or other myriad NodeLoader derivatives from a loop through factoryMap's contents.
Of course, factoryMap could just as well be a vector or whatever you prefer, since it isn't required for searching, it's just a collection of what to make (and, due to the template NodeFactory, how to make them).