How to create list of classes at compile time?

I'm trying to figure out a maintainable way to have an XML loader be able to lookup loaders for subsections of a document based on a string key.

I have a data structure made up of nodes. As I scan through a certain section of an XML document, I want to be able to look up a loader based on a tag name and hand off processing to that loader to load that particular node. The code looks something like this:


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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
//Nodes

class Node
{
};

class RectNode : public Node
{
    int _x;
    int _y;
    int _width;
    int _height;
public:
    RectNode() {}
};

class TextNode : public Node
{
    QString _text;
public:
    TextNode() {}
};


//Loaders

class NodeLoader
{
    QString _tagName;
public:
    NodeLoader(QString& tagName) : _tagname(tagName) {}
    virtual Node* load(QDomElement& ele) {}
};

class RectLoader : public NodeLoader
{
    static const QString ID; // = "rect"
public:
    RectLoader() : NodeLoader(ID) {}
    Node* load(QDomElement& ele) override {
        RectNode* r = new RectNode();
        ...
        return r;
    }
};

class TextLoader : public NodeLoader
{
    static const QString ID; // = "text"
public:
    TextLoader() : NodeLoader(ID) {}
    Node* load(QDomElement& ele) override {
        TextNode* r = new TextNode();
        ...
        return r;
    }
};


Now the loader will need to have an index of all these loaders somewhere that it will be able to find. I could do something like this:

1
2
3
4
5
6
7
8
9
10
11
12

class XmlReader
{
    QMap<QString, NodeLoader*> _loaderMap;
public:
    void init()
    {
        _loaderMap[RectLoader::ID] = new RectLoader();
        _loaderMap[TextLoader::ID] = new TextLoader();
        ...
    }
};


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?
I am not 100% sure what you just asked.

There are tools that can tell you class hierarchy, you compiler isnt one. Is that what you want?

The compiler eliminates unused methods in the executable, if it can. But they slow down the compiler on big projects and sometimes it can't tell something is not used.

No, I'm more looking for a way to automatically create the XmlReader::init() method (or something equivilant) without having to hand edit it each time I create or delete a new loader class. If the compiler could somehow build this index for me, it would make development easier.

For example, maybe I could have a static object somewhere and there would be some line I could put in the header file containing RectNodeLoader that will cause it to register itself with this static object? That way I could put the 'add to list' code next to my class definition rather than having to hunt down XmlReader::init() to put it there.

Last edited on
you could loop it, from the looks of it, with a little damage..

TextLoader::ID <---- this should all be stored in a single container, and it should have ID and type.
then in init:
for (all the ids in your container)
_loaderMap[container::id] = new container::type();

which you can't do exactly like that (you need a way to reconcile the type code you stored to a pointer) but its doable. But you just moved the problem, now you have to constantly update your container of ID/types. Is that any better? Presumably it would be a slight improvement as you were creating thing::id somewhere each time?
Last edited on
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).
Last edited on
Thanks for your suggestion, Niccolo! This is exactly what I was looking for.
Topic archived. No new replies allowed.