3

I've got an assignment to create a sort of a multi-platform C++ GUI library. It wraps different GUI frameworks on different platforms. The library itself provides an interface via which the user communicates uniformly regardless of the platform he's using.

I need to design this interface and underlying communication with the framework properly. What I've tried is:

  1. Pimpl idiom - this solution was chosen at first because of its advantages - binary compatibility, cutting the dependency tree to increase build times...
class Base {
public:
    virtual void show();
    // other common methods
private:
    class impl;
    impl* pimpl_;
};

#ifdef Framework_A
class Base::impl : public FrameWorkABase{ /* underlying platform A code */ };
#elif Framework_B
class Base::impl : public FrameWorkBBase { /* underlying platform B code */ };
#endif

class Button : public Base {
public:
    void click();
private:
    class impl;
    impl* pimpl_;
};

#ifdef Framework_A
class Button::impl : public FrameWorkAButton{ /* underlying platform A code */ };
#elif Framework_B
class Button::impl : public FrameWorkBButton { /* underlying platform B code */ };
#endif

However, to my understanding, this pattern wasn't designed for such a complicated hierarchy where you can easily extend both interface object and its implementation. E.g. if the user wanted to subclass button from the library UserButton : Button, he would need to know the specifics of the pimpl idiom pattern to properly initialize the implementation.

  1. Simple implementation pointer - the user doesn't need to know the underlying design of the library - if he wants to create a custom control, he simply subclasses library control and the rest is taken care of by the library
#ifdef Framework_A
using implptr = FrameWorkABase;
#elif Framework_B
using implptr = FrameWorkBBase;
#endif

class Base {
public:
    void show();
protected:
    implptr* pimpl_;
};

class Button : public Base {
public:
    void click() {
#ifdef Framework_A
        pimpl_->clickA(); // not working, need to downcast
#elif Framework_B
        // works, but it's a sign of a bad design
        (static_cast<FrameWorkBButton>(pimpl_))->clickB();
#endif
    }
};

Since the implementation is protected, the same implptr object will be used in Button - this is possible because both FrameWorkAButton and FrameWorkBButton inherit from FrameWorkABBase and FrameWorkABase respectively. The problem with this solution is that every time i need to call e.g. in Button class something like pimpl_->click(), I need to downcast the pimpl_, because clickA() method is not in FrameWorkABase but in FrameWorkAButton, so it would look like this (static_cast<FrameWorkAButton>(pimpl_))->click(). And excessive downcasting is a sign of bad design. Visitor pattern is unacceptable in this case since there would need to be a visit method for all the methods supported by the Button class and a whole bunch of other classes.

Can somebody please tell me, how to modify these solutions or maybe suggest some other, that would make more sense in this context? Thanks in advance.

EDIT based od @ruakh 's answer

So the pimpl solution would look like this:

class baseimpl; // forward declaration (can create this in some factory)
class Base {
public:
    Base(baseimpl* bi) : pimpl_ { bi } {}
    virtual void show();
    // other common methods
private:
    baseimpl* pimpl_;
};

#ifdef Framework_A
class baseimpl : public FrameWorkABase{ /* underlying platform A code */ };
#elif Framework_B
class baseimpl : public FrameWorkBBase { /* underlying platform B code */ };
#endif


class buttonimpl; // forward declaration (can create this in some factory)
class Button : public Base {
public:
    Button(buttonimpl* bi) : Base(bi), // this won't work
                             pimpl_ { bi } {}
    void click();
private:
    buttonimpl* pimpl_;
};

#ifdef Framework_A
class Button::impl : public FrameWorkAButton{ /* underlying platform A code */ };
#elif Framework_B
class Button::impl : public FrameWorkBButton { /* underlying platform B code */ };
#endif

The problem with this is that calling Base(bi) inside the Button's ctor will not work, since buttonimpl does not inherit baseimpl, only it's subclass FrameWorkABase.

mrdecompilator
  • 155
  • 1
  • 8
  • 1
    You can't have two distinct definitions of `Base::impl` (or, in your second approach, `impl`) in the same program. Depending on how you do that, the result is either a diagnosable error (if both definitions are visible in the same compilation unit) or undefined behaviour (if the definitions are in different compilation units). – Peter Mar 19 '20 at 08:04
  • It seems like you're using inheritance for things it's not really needed for. For example, why do you need the `FrameWorkABase` base class? – ruakh Mar 19 '20 at 08:07
  • @ruakh To represent a base object for GUI control - e.g. in Qt it's QWidget. Then you can define methods like show() or getPosition() common for all the controls. – mrdecompilator Mar 19 '20 at 08:13
  • Looking at this piece of code in Base: impl* pimpl_; In places where your code is aware of FrameWorkAButton, why wouldn't you use this instead: FrameWorkAButton* pimpl_; ? – armagedescu Mar 19 '20 at 09:03
  • @ruakh according to your answers here, FrameWorkABase is a general GUI control object. If this is the case, I guess each control object is clickable, which means FrameWorkABase class should have a virtual `click()` function and each subclass should implement its own click method. This way you use polymorphism to call a specific behaviour without downcasting (which in this case is a bad design because it creates dependencies) – SubMachine Mar 19 '20 at 12:24
  • `click()` was supposed to be a method which is specific only for buttons, but I guess that was a bad example. What about `setValue()` for spinbox, or `addItem()` for list box? These cannot be in the `Base` class. – mrdecompilator Mar 19 '20 at 13:10
  • So, to be clear -- is `FrameWorkBButton` (for example) a class that *you're* creating, to be a shim between your `Button` class and the underlying framework's button functionality? Or is it a class in the underlying framework? I ask because in the former case I don't understand why `FrameWorkAButton` and `FrameWorkBButton` expose different interfaces, and in the latter case I don't think you can assume that every underlying framework has the same class hierarchy (or even has *any* class hierarchy -- what if it's written to be compatible with C)? – ruakh Mar 20 '20 at 17:58
  • @ruakh `FrameWorkAButton` represents the framework's UI button which is displayed in the UI. You can imagine `Framework_A` being Qt, `FrameWorkABase` being `QWidget` and `FrameWorkAButton` being `QPushButton`. In Qt, the `QPushButton` inherits from `QWidget`, therefore i can use this type of polymorphism. and this type of hierarchy applies to every framework I intend to use. My classes `Base` or `Button` just represent an interface, through which I'm able to communicate with each of the framework's controls. – mrdecompilator Mar 21 '20 at 06:51
  • I think you are looking for inline namespaces. – L. F. Mar 21 '20 at 10:36
  • @L.F. I'm not entirely sure how you think I should use them. They would help me with versioning of my UI library, but how does that solve my design issue regarding class hierarchy? – mrdecompilator Mar 21 '20 at 11:13

1 Answers1

1

The problem with this solution is that every time i need to call e.g. in Button class something like pimpl_->click(), I need to downcast the pimpl_, because clickA() method is not in FrameWorkABase but in FrameWorkAButton, so it would look like this (static_cast<FrameWorkAButton>(pimpl_))->click().

I can think of three ways to solve that issue:

  1. Eliminate Base::pimpl_ in favor of a pure virtual protected function Base::pimpl_(). Have subclasses implement that function to provide the implementation pointer to Base::show (and any other base-class functions that need it).
  2. Make Base::pimpl_ private rather than protected, and give subclasses their own appropriately-typed copy of the implementation pointer. (Since subclasses are responsible for calling the base-class constructor, they can ensure that they give it the same implementation pointer as they plan to use.)
  3. Make Base::show be a pure virtual function (and likewise any other base-class functions), and implement it in subclasses. If this results in code duplication, create a separate helper function that subclasses can use.

I think that #3 is the best approach, because it avoids coupling your class hierarchy to the class hierarchies of the underlying frameworks; but I suspect from your comments above that you'll disagree. That's fine.


E.g. if the user wanted to subclass button from the library UserButton : Button, he would need to know the specifics of the pimpl idiom pattern to properly initialize the implementation.

Regardless of your approach, if you don't want the client code to have to set up the implementation pointer (since that means interacting with the underlying framework), then you will need to provide constructors or factory methods that do so. Since you want to support inheritance by client code, that means providing constructors that handle this. So I think you wrote off the Pimpl idiom too quickly.


In regards to your edit — rather than having Base::impl and Button::impl extend FrameworkABase and FrameworkAButton, you should make the FrameworkAButton be a data member of Button::impl, and give Base::impl just a pointer to it. (Or you can give Button::impl a std::unique_ptr to the FrameworkAButton instead of holding it directly; that makes it a bit easier to pass the pointer to Base::impl in a well-defined way.)

For example:

#include <memory>

//////////////////// HEADER ////////////////////

class Base {
public:
    virtual ~Base() { }
protected:
    class impl;
    Base(std::unique_ptr<impl> &&);
private:
    std::unique_ptr<impl> const pImpl;
};

class Button : public Base {
public:
    Button(int);
    virtual ~Button() { }
    class impl;
private:
    std::unique_ptr<impl> pImpl;
    Button(std::unique_ptr<impl> &&);
};

/////////////////// FRAMEWORK //////////////////

class FrameworkABase {
public:
    virtual ~FrameworkABase() { }
};

class FrameworkAButton : public FrameworkABase {
public:
    FrameworkAButton(int) {
        // just a dummy constructor, to show how Button's constructor gets wired
        // up to this one
    }
};

///////////////////// IMPL /////////////////////

class Base::impl {
public:
    // non-owning pointer, because a subclass impl (e.g. Button::impl) holds an
    // owning pointer:
    FrameworkABase * const pFrameworkImpl;

    impl(FrameworkABase * const pFrameworkImpl)
        : pFrameworkImpl(pFrameworkImpl) { }
};

Base::Base(std::unique_ptr<Base::impl> && pImpl)
    : pImpl(std::move(pImpl)) { }

class Button::impl {
public:
    std::unique_ptr<FrameworkAButton> const pFrameworkImpl;

    impl(std::unique_ptr<FrameworkAButton> && pFrameworkImpl)
        : pFrameworkImpl(std::move(pFrameworkImpl)) { }
};

static std::unique_ptr<FrameworkAButton> makeFrameworkAButton(int const arg) {
    return std::make_unique<FrameworkAButton>(arg);
}

Button::Button(std::unique_ptr<Button::impl> && pImpl)
    : Base(std::make_unique<Base::impl>(pImpl->pFrameworkImpl.get())),
      pImpl(std::move(pImpl)) { }
Button::Button(int const arg)
    : Button(std::make_unique<Button::impl>(makeFrameworkAButton(arg))) { }

///////////////////// MAIN /////////////////////

int main() {
    Button myButton(3);
    return 0;
}
ruakh
  • 175,680
  • 26
  • 273
  • 307
  • Thanks for the response. As for the 1st point - ofc this would minimise the downcast count, but the downcasting itself would need to be done in the pimpl() method anyway. 2nd point reminds me of the pimpl idiom solution a bit and I like it the most out of the three, because the 3rd point is not fully in line with the OOP. That comment about factories and constructors got me thinking tho and I think this is the way to go. BUT, i would need a forward declaration of the `impl` class to be able to create its objects outside e.g. `Button` class wouldn't I? (not a problem, just reassuring) – mrdecompilator Mar 22 '20 at 08:14
  • And by "forward declaration" I mean declaration outside the class (not as a private field) - so that that class can be instantiated in factory. Also, all the private variables would need to go from `Base` and `Button` classes to their implementations (this would create duplicated data - e.g. `Base` has private field "event handlers" which is a map of user defined functions triggered by event like button click - this would need to be declared in every `Base::impl` class for every framework). – mrdecompilator Mar 22 '20 at 09:10
  • Furthermore, i guess there couldn't be `Base::impl`, since the `Button::impl` would no more inherit from `Base`'s implementation, because `Base::impl : FrameworkABase`, `Button::impl : FrameworkAButton` and `FrameworkAButton : FrameworkABase`. Therefore i wouldn't be able to pass `Button::impl` to `Base`'s constructor and downcast it there to `Button::impl`. I proprose, that `Base` wouldn't implement pimpl idiom, it would have basic pointer like in my questions's 2. solution, but `Base`'s subclasses on the other hand would implement pimpl idiom. – mrdecompilator Mar 22 '20 at 09:22
  • Re: "As for the 1st point - ofc this would minimise the downcast count, but the downcasting itself would need to be done in the pimpl() method anyway": Not at all; I'm not sure why you say that. Since the pimpl field is in the subclass, it has the right type to begin with; no downcasting is needed. – ruakh Mar 22 '20 at 16:15
  • Ah yeah sure you are right - due to a covariant return type. – mrdecompilator Mar 22 '20 at 19:37
  • I edited my question - added pimpl solution which illustrates the problem I described in the comments above. – mrdecompilator Mar 24 '20 at 06:51
  • @mrdecompilator: I've edited my answer to respond to your edit. – ruakh Mar 27 '20 at 03:47
  • But now you cannot override any of the `FrameworkAButton`'s methods already implemented by the framework. I could create something like `AbstractButton::impl`, where its `pFrameworkImpl` member would be `QAbstractButton` in Qt. Then I would be able to extend `AbstractButton` by `RadioButton` or `PushButton`. `RadioButton::impl`'s `pFrameworkImpl` could then extend `QRadioButton`. But this is allowed is my solution as well, where `Button::impl` represents framework's control, without the `pFrameworkImpl`. I will accept your answer anyway, thanks for the help. – mrdecompilator Apr 01 '20 at 06:18