1

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.

Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
Lake
  • 21
  • 1
  • 4
  • 2
    That looks like the Factory design pattern. It is a well known solution to the problem where you need to do extra work *after* the constructor but *before* it becomes available to the user. I wouldn't consider it hacky. – François Andrieux Nov 04 '21 at 17:54
  • 2
    It's not about vtables; that's an implementation detail. The fundamental rule in C++ is that you can't use an object that hasn't been constructed. So derived class constructors don't run until their bases have all been constructed. If you try to fight this rule you'll lose. – Pete Becker Nov 04 '21 at 17:55
  • The reason you can't access derived type's override during construction is by design. At that point there is no member of that derived type that has been initialized. The derived part of the object hasn't even begun to start its lifetime yet. – François Andrieux Nov 04 '21 at 17:55
  • 3
    You could also use the [CRTP](https://stackoverflow.com/questions/4173254/what-is-the-curiously-recurring-template-pattern-crtp) if you require derived types to provide a `static` member function to perform the initialization, to which you can pass a reference to the base type. But this pattern is arguably less well known than the Factory design pattern. That solution also makes more difficult the use of `Window` as a common interface. – François Andrieux Nov 04 '21 at 17:57
  • 1
    You can partially invert the order using the initialization list in the derived class. You can pass the "Product" of initialization to the base class through its constructor. Or it could be a function pointer that the base should call to do the initialization at the right moment. Or use CRTP. – Quimby Nov 04 '21 at 17:58
  • @FrançoisAndrieux I went and brushed up on my understanding of the Factory design pattern but I'm still not entirely sure how that's any different than simply using my static "Create" that I'm using now. I think my issue is that I want to ensure a method runs after all ctors and another method before all dtors. And I'm asking if there is any better alternative other than using a static Create method (to run something just after ctors), and also, what about just before dtors? shared_ptr with a custom deleter? – Lake Nov 04 '21 at 18:27
  • 1
    @Lake You misunderstood my comment. I was saying your `Create` function **is** an instance of the Factory design pattern. It was meant to reassure you that the strategy isn't very strange. – François Andrieux Nov 04 '21 at 18:29

2 Answers2

0

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{});
}
Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • So, after reading the code like 20 times, I think your answer means that I should have two classes, a UI class that gets executed by the ui thread, and a class that creates the ui thread. So basically a class "UIThreadCreator" and a "WindowUI" and an "OverlayUI : WindowUI". This way, UIThreadCreator (can be template) won't have to be subclassed if I simply wanted to "override" UIInit. Did I understand your answer correctly? – Lake Nov 04 '21 at 18:49
0

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.

Lake
  • 21
  • 1
  • 4