c++pointersmemory-managementraiiownership-semantics

How to organize object ownership for class that lives lesser time than owner of the object?


I have the following situation: there is class of GraphicsContext:

class GraphicsContext {
    ...
private:
    std::unique_ptr<Renderer> m_renderer;
}

And there is a class of application that uses the GraphicsContext:

class Application {
   ...
private:
    std::unique_ptr<GraphicsContext> m_graphicsContext;
}

And there are sub-level classes those are used in Application class and those uses Renderer from the GraphicsContext. I need to store pointer to the renderer in these classes, but how should I do that?

class SubLevelClass {
public:
    SubLevelClass(Renderer* renderer);
    ...
    void drawSomething();
private:
    Renderer* m_renderer; // this class is not owner of Renderer but should can ability to refer to it
}

Such sub-level classes does not semantically own the Renderer and therefore I think it't not good idea to use shared_ptr instead of unique_ptr. But how to organize such ownership if it's garanteed that objects of sub-level classes live lesser time than Application object? Can I store and return from GraphicsContext a raw pointer to Renderer or it's semantically wrong idea?


Solution

  • There are some ways that can solve it.

    These codes are not tested but should be enough to show the idea.

    Solution 1 : plainly keep track

    Solution 1A :-

    class Renderer{
        std::vector<SubLevelClass*> whoThatLinkBackToMe; //or std::unordered_set
        public: ~Renderer(){
            //assert that whoThatLinkBackToMe.size() == 0
        }
    };
    class SubLevelClass{
        SubLevelClass(Renderer* renderer){
            renderer->whoThatLinkBackToMe.push_back(this);
        }
        //.. destructor should remove "this" from "renderer->whoThatLinkBackToMe" too
    };
    

    Solution 1B :-

    class CentralizeSystem{
        public: static std::unordered_map<Renderer*,SubLevelClass*> map;
    };
    class Renderer{
        public: ~Renderer(){
            //assert that CentralizeSystem::map[this].size() == 0
        }
    };
    class SubLevelClass{
        SubLevelClass(Renderer* renderer){
            CentralizeSystem::map.add(renderer,this);
        }
        //.. destructor should remove "this" from "CentralizeSystem::map" too
    };
    

    Solution 2 : Entity Component System (ECS)

    It is a revolution in design that require a huge commitment :-

    1. Make Renderer a System in ECS. Thus, it is automatically deleted last.
    2. Make SubLevelClass a Component in ECS. Try to store all information (field, cache) in SubLevelClass - not Renderer. These 2 things alone should solve your issue.
    3. However, if it is very unlucky e.g. you need to make Renderer not singleton (become Component):

      3.1 Create a new component Component_CheckDelete e.g. :-

      class Component_CheckDelete : public Component{
          public: bool ToBeDeleted=false;
      };
      

      3.2 Whenever a Renderer is to be deleted, just mark its Component_CheckDelete::ToBeDeleted=true.
      Then, in the end of time-step, check every instance of SubLevelClass.
      If there are some SubLevelClass that refer to the Renderer that has convertToComponent<Component_CheckDelete>(rendererPtr)->ToBeDeleted==true, throw assert fail.

    Solution 3

    Just ignore the whole issue.
    It is an error at the user's side. Engine creator is not supposed to catch every user's mistake.

    Bullet Physics (one of the best Physics Engine) uses this approach a lot - if I delete its boardphase module and still use its main engine, I can get an irresponsible access violation.

    My opinion : I usually picks Solution 3, sometimes Solution 2, rarely pick Solution 1A.