C++ architecture: multi-domain singletons?

DSP, Plugin and Host development discussion.
RELATED
PRODUCTS

Post

This isn't strictly plugin specific, but it's something that comes up often anyway, so I figured it'd be related enough for this forum.

The basic problem: we have a bunch of "component types" where components of the same type want to share some resources. The standard solution for this situation (besides letting "application" track the crap, which violates encapsulation and separation of concerns) is to put the resources into a singleton object. So far so good.

The multi-domain version: we still have a bunch of component types that still want to share resources, but now each component belongs to a "domain" (eg. plugin, GUI window, etc) and we need a separate instance of the resources on per-domain basis (eg. maybe they are GPU resources tied to the GPU context of a given window).

One (type-safe) solution I can think of is to make a "domain-aware singleton manager" that looks something like a global hash-table mapping from a domain to a "singleton instance" for that domain. Then you call "getObjectForDomain()" or something and the manager would then create an object if necessary and register it with the domain for destruction, but something in the back of my mind says this is a bit on the ugly side of things.

So.. can someone think of a cleaner way to do this in C++ that does not involve making either the "domains" (eg. the window implementation or whatever) or the application code aware of the specific types of resources we need to track? Is there a standard design-pattern for this that I've somehow missed? Maybe my solution is already the "best" there is and I'm just being silly by rejecting it (well, I'll probably use it if I can't come up with anything cleaner)?

Post

After failing to find a "clean" solution this is what I have decided to do about singletons: Register a single exit method with "atexit" and keep a list specifying the order with which the resources will be cleaned. When a singleton is created, it registers itself with the list, specifying an ID akin to the "domain" you mention and each domain ID has an integer value chosen according to the proper order of resource clean up.
~stratum~

Post

stratum wrote:After failing to find a "clean" solution this is what I have decided to do about singletons: Register a single exit method with "atexit" and keep a list specifying the order with which the resources will be cleaned. When a singleton is created, it registers itself with the list, specifying an ID akin to the "domain" you mention and each domain ID has an integer value chosen according to the proper order of resource clean up.
Cleanup is not a problem, that's the easy part: just store a callback interface into a list in the domain itself to have it notify all the relevant objects when it gets destroyed. I don't need (or want) any determinism with regards to cleanup order beyond this.

Rather it's the creation part that I'm thinking about. Mapping from pointers to domains using a hash table is not really a problem except as far as having such global hash tables lying around everywhere seems sort of ugly. So I wonder if Modern C++ with all it's new wonderful productivity features might have a solution to this.

Post

I have never had problems with singleton initialization.
Not that it's problem free but the problem is easy to solve: Just make sure that the initialization is performed only once in a multithreaded environment. This is the only problem that actually needs to be solved about it, what looks ugly or nice is a matter of taste and usually I don't care about such things.
~stratum~

Post

"Not that it's problem free but the problem is easy to solve: Just make sure that the initialization is performed only once in a multithreaded environment"
Not anymore a problem in c++ 11, the standard guarantee that.

Now the most stupid way also became the right way to do it :
static Singleton& GetInstance()
{
static Singleton instance;
return instance;
}

Post

Well, after realising that this problem is essentially isomorphic to "component entity systems" I just implement a "component entity system" of the type where an "entity" is really just a glorified ID.

Post

mystran wrote:The multi-domain version: we still have a bunch of component types that still want to share resources, but now each component belongs to a "domain" (eg. plugin, GUI window, etc) and we need a separate instance of the resources on per-domain basis (eg. maybe they are GPU resources tied to the GPU context of a given window).
I think I'm missing why you want to have a singleton instance per domain, can you elaborate on your GPU resource example?

I limit myself to const singletons, so I guess I haven't run into this issue. Or maybe I misunderstood the problem.

Post

mtytel wrote:
mystran wrote:The multi-domain version: we still have a bunch of component types that still want to share resources, but now each component belongs to a "domain" (eg. plugin, GUI window, etc) and we need a separate instance of the resources on per-domain basis (eg. maybe they are GPU resources tied to the GPU context of a given window).
I think I'm missing why you want to have a singleton instance per domain, can you elaborate on your GPU resource example?

I limit myself to const singletons, so I guess I haven't run into this issue. Or maybe I misunderstood the problem.
Well, I think me talking about singletons has confused everyone, but I realised that I was really just dreaming about an entity component system (which are popular for game development these days). So that's what I implemented.

Basically I want to do something like this (and this is now working code):

Code: Select all

// define the component data and create a manager
struct Comment { std::string s; };
ComponentManager<Comment> cmComment;

// use the components: they are created first time you ask for one, cleaned up when the host dies
const std::string & getComment(ComponentHost * obj) { return cmComment.getComponent(obj)->s; }
void setComment(ComponentHost * obj, const std::string & s) { cmComment.getComponent(obj)->s = s; }
Implementation of ComponentHost (aka. entity) looks like this (should probably be made non-copyable but whatever):

Code: Select all

    // host is a simple object with just a vtable
    struct ComponentHost
    {
        virtual ~ComponentHost() { destroyComponents(); }
        void destroyComponents() { ComponentSystem::destroyComponents(this); }
    };
So any object can derive from ComponentHost at which point any other code anywhere can attach arbitrary components to it that get automatically cleaned up when it is time to do so. Curiously this doesn't even need any singletons under the hood, just some regular globals with no dependencies with regards to construction/destruction order.

Post

Ah I didn't know you were talking about that type of component design. Why do you need the managers though?

What's wrong with:

Code: Select all

    // Half-assed  implementation of a base class for component based design
    class ComponentHost
    {
    public:
        template<class C>
        C* addComponent() {
            C* c = new C();
            components_.push_back(c);
            return c;
        }
        template<class C>
        C* getComponent() { 
            // Loop with dynamic casts or use lookup using class id?
        }

        virtual ~ComponentHost() { destroyComponents(); }
        void destroyComponents() { ... }

    private:
        std::vector<Component*> components_;
    };

Post

mtytel wrote:Ah I didn't know you were talking about that type of component design. Why do you need the managers though?
Your suggestion is very similar to what I've seen described as "poor man's ECS" in a bunch of articles on the subject (eg. typically it's described as a sort of "middle-ground" between OOP and "true ECS"). That doesn't mean it's necessarily a terrible design (it's not), but the way I see it decoupling the components completely into separate managers has (at least) the following advantages:

1. the "entity" itself is just about as lightweight as it can get (well, you could make it a pure ID, but I'm not convinced this is worth the extra trouble it brings) so there is very little cost in making just about everything an "entity"

2. the manager itself acts as a symbolic name for the components that compiler can error check: it's impossible to lookup "invalid" components without getting a compilation error and if you try to declare two managers with the same name in two different places you get a link-time error even if it happens to compile; you can still have multiple managers with the same underlying C++ type if you want, since this just means you have two logically separate components that happen to share the underlying type

3. you can trivially loop through all the components of a given manager without having to loop through all the components of all the entities (you don't even need to keep a list of entities in the first place)

4. it is completely statically type-safe at compile time; no need for any runtime-checking or dynamic_cast anywhere ever (you can even compile without RTTI)

There's probably something else I forgot.

Just about the only downside that comes to mind is that finding all the components of a given entity takes a bit more effort since you have to ask all the managers unless you keep a list of components either in the entity or a suitable "meta-component" (which is the less intrusive option since it doesn't bloat the entity itself).

Post


Post

Watch out Mystran, I had exactly the same problem as you and started using the same kind of solution. Now I find myself slipping slowly towards functional programming approaches, i.e. all data is immutable and I just pass data pointers to "black-box systems" which use the data however they wish (without modifying the source data ofcourse). But then again, this is also becoming a trend in games programming as far as I've heard :)

In short: using classes is becoming less and less common in my code. I'm still in the middle of the trasition process and I'm not sure where it's taking me... :o

Post

Kraku wrote:Watch out Mystran, I had exactly the same problem as you and started using the same kind of solution. Now I find myself slipping slowly towards functional programming approaches, i.e. all data is immutable and I just pass data pointers to "black-box systems" which use the data however they wish (without modifying the source data ofcourse). But then again, this is also becoming a trend in games programming as far as I've heard :)
I've been slipping slowly towards functional programming approaches for the past 15 years anyway, so I'm not too worried about that. I'm not very pedantic about it, but in general I try to prefer interfaces that pretend they are just manipulating simple stack objects in a more or less functional way, even if behind the curtains monsters are often running loose. I find that this usually both reduces application level boilerplate code and makes it easier to change the underlying implementation (eg. when performance becomes an issue), so it's kinda win-win situation usually.

Post

I have done some functional programming with Erlang and seen for what it is useful for, but couldn't see how the same benefits could be obtained from C++ code which provides numerous ways to shoot yourself in the foot. As long as one is doing functional programmig for the sake of doing it for aesthetic reasons, probably it doesn't matter, but if that code will be running nonstop without ever failing then "functional programming done in C++" sounds like saying "I have only one tool and want to use it for everything and don't like looking at others at all, and I'm willing to face the consequences".
~stratum~

Post

mystran wrote:
stratum wrote:After failing to find a "clean" solution this is what I have decided to do about singletons: Register a single exit method with "atexit" and keep a list specifying the order with which the resources will be cleaned. When a singleton is created, it registers itself with the list, specifying an ID akin to the "domain" you mention and each domain ID has an integer value chosen according to the proper order of resource clean up.
Cleanup is not a problem, that's the easy part: just store a callback interface into a list in the domain itself to have it notify all the relevant objects when it gets destroyed. I don't need (or want) any determinism with regards to cleanup order beyond this.
It may, for instance if one of your singletons has a reference to another library that will be unloaded before the singleton's, you will want to clear the singleton first.

Otherwise, yes, a main singleton per module is fine.

Post Reply

Return to “DSP and Plugin Development”