问题
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:
- 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.
- 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
.
回答1:
The problem with this solution is that every time i need to call e.g. in
Button
class something likepimpl_->click()
, I need to downcast thepimpl_
, becauseclickA()
method is not inFrameWorkABase
but inFrameWorkAButton
, so it would look like this(static_cast<FrameWorkAButton>(pimpl_))->click()
.
I can think of three ways to solve that issue:
- 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).
- 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.)
- 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;
}
来源:https://stackoverflow.com/questions/60752964/how-to-avoid-downcasting-in-this-specific-class-hierarchy-design