Home > OS >  A less hacky way to call ctor after all superclass ctors, and dtor before all superclass dtors
A less hacky way to call ctor after all superclass ctors, and dtor before all superclass dtors

Time:11-06

I know that before the superclass ctor is finished, the subclass (and vtable) is not yet complete. But, I have a situation where, logically, I must be able to change the initialization of the superclass, and I'm trying to figure out how I can do that.

Here is a simplified example:

class Window {
   std::thread uiThread;
   void uiThreadProc(){
       try {
           UIInit();
           NotifyOtherThreadThatUIInitFinished();
       } catch(...){
           UIClean();
           throw; // actually using an exception_ptr but it's irrelevant here
       }

       EventLoop();
       UICleanup();
   }
   virtual void UIInit();
   virtual void UICleanup();
   Super(){
       CreateUIThread();
       WaitForUIThreadToFinishUIInit();
   }
   ~Super(){
       SendQuitEventToUIThread();
       WaitForUIThreadToFinish();
   }
}

class Overlay : public Window {
    Overlay(){
        // can't do stuff before UIInit because Window ctor returns when UIInit finishes
    }
    void UIInit() override {
        // won't be called because UIInit is called before Window ctor returns
    }
    void UIClean() override {
        // won't get called if UIInit throws
    }
    ~Overlay(){}
}

Currently, I'm trying to make it work by making the ctors private and moving the logic to an Init method that gets called after ctors, like this:

class Window {

   static std::shared_ptr<Window> Create(){
       auto window = std::make_shared<Window>();
       window->Init();
       return window;
   }
   virtual void Init() { /* actual ctor content here */ }
   virtual void Cleanup() { /* actual dtor content here */}
   bool cleanupCalled = false;
   ~Window(){
      if(!cleanupCalled){
          cleanupCalled = true; // with mutex and locks...
          Cleanup(); 
      }
   }
}

class Overlay : public Window {
    // same as above, but now overriding Init() and Cleanup()...
}

I assume this would work, but it feels super hacky and convoluted.

And I don't understand why this is the case from a design perspective, why don't ctors first create the complete vtable, then call the hierarchy of ctors? The vtable doesn't depend on member initialization, so it won't break things to do it first, correct?

Is this not a good use for inheritance?

I need to be able to override UIInit because some things must run on the UI thread. I could send it as functions/events and the EventLoop would execute that, but that would seriously break the logic of "when ctor finishes, the object is fully initialized", or if it absolutely has to run before the EventLoop. I could make my own thread event handling class, but that simply seems wrong for such a trivial case.

Any architectural suggestions are very welcome.

CodePudding user response:

This can be solved by using composition instead of inheritance.

Furthermore, UIInit() and UICleanup() are begging to be RAII-driven. We can do this by separating Overlay in two types: The config, meant to live on the main thread, and a "Runtime" subtype that gets to live in the Window's thread.

#include <concepts>
#include <utility>

// Ignore these concepts pre-c  20
template<typename T, typename Owner>
concept WindowControllerRuntime = std::constructible_from<T, Owner*> && 
  requires(T r) {
    // Use this in lieu of pure virtual functions
    {r.update()};
  };

template<typename T>
concept WindowController = WindowControllerRuntime<typename T::Runtime, T>;

class Window {
public:
  virtual ~Window() = default;
};

// pre-c  20: use template<typename ControllerT> instead
template<WindowController ControllerT>
class WindowWithController : public Window {
    ControllerT controller_;
public:
    WindowWithController(ControllerT controller) 
      : controller_(std::move(controller)) {
        CreateUIThread();
        WaitForUIThreadToFinishUIInit();
    }

    ~WindowWithController() {
       SendQuitEventToUIThread();
       WaitForUIThreadToFinish();
    }

   void uiThreadProc(){
     typename ControllerT::Runtime runtime(&controller_);
     NotifyOtherThreadThatUIInitFinished();
       
     EventLoop();
     UICleanup();
   }

   private:
    void SendQuitEventToUIThread();
    void WaitForUIThreadToFinish();

    void CreateUIThread();
    void WaitForUIThreadToFinishUIInit();
    void NotifyOtherThreadThatUIInitFinished();
    void EventLoop();
    void UICleanup();
};


// Usage example
#include <memory>

class Overlay {
public:
    struct Runtime {
        Runtime(Overlay* owner) {} // UIInit
        ~Runtime() {} //UIClean

        void update() {}
    };
};

int main() {
    std::unique_ptr<Window> w = std::make_unique<WindowWithController<Overlay>>(Overlay{});
}

CodePudding user response:

What I ended up using is this:

std::shared_ptr<Overlay> Overlay::Create() {

    std::shared_ptr<Overlay> ptr(new Overlay(), [](Overlay* toDelete) {
        toDelete->Cleanup();
        delete toDelete;
        });
    ptr->Init();
    return ptr;
}

But this has it's limitations:

  • I can't allocate an Overlay on the stack (because then I would have to call Init and Cleanup manually, and the whole point is to automate that).
  • I have to copy paste the Create method in every subclass of Window. Maybe there is a way to automate that with templates but I couldn't do it without making the whole class a template.

I'm not going to accept my answer just yet, maybe someone will have a better solution.

Opinion: I am a beginner in C , but honestly, it seems to me like a huge design flaw not to be able to call virtual methods in the contructor/destructor. Other languages do it just fine (cough, Java, cough). And I don't see why it wouldn't work with a stack allocation as well as a heap allocation. You could literally do it manually (real ctor, fake ctor, dostuff, fake dtor, dtor) so the compiler could have done the same thing (allocate memory, fill vtable, ctor of base, ctor of sub, do stuff, dtor of sub, dtor of base, deallocate memory). And that way, the base can give a chance to the sub to do stuff before/after the base's ctor/dtor. But anyway, maybe there is some technical reason I'm missing.

  • Related