如何避免在这个特定的 class 层次结构设计中向下转型?
How to avoid downcasting in this specific class hierarchy design?
我有一项任务是创建一种多平台 C++ GUI 库。它在不同的平台上封装了不同的 GUI 框架。该库本身提供了一个界面,用户可以通过该界面统一通信,而不管他使用的是什么平台。
我需要正确设计此接口以及与框架的底层通信。我试过的是:
- Pimpl 惯用法 - 最初选择此解决方案是因为它的优点 - 二进制兼容性、削减依赖树以增加构建时间...
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
但是,据我了解,此模式并不是为您可以轻松扩展接口对象及其实现的复杂层次结构而设计的。例如。如果用户想要从库 UserButton : Button
中子 class 按钮,他需要知道 pimpl 习语模式的细节才能正确初始化实现。
- 简单的实现指针——用户不需要知道库的底层设计——如果他想创建一个自定义控件,他只需简单地subclasses库控件,剩下的就搞定了来自图书馆
#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
}
};
由于实现受到保护,相同的 implptr
对象将用于 Button
- 这是可能的,因为 FrameWorkAButton
和 FrameWorkBButton
都继承自 FrameWorkABBase
和 FrameWorkABase
分别。这个解决方案的问题是每次我需要打电话给在 Button
class 类似 pimpl_->click()
中,我需要向下转换 pimpl_
,因为 clickA()
方法不在 FrameWorkABase
中,而是在 FrameWorkAButton
,所以它看起来像这样 (static_cast<FrameWorkAButton>(pimpl_))->click()
。过度向下转型是糟糕设计的标志。在这种情况下,访问者模式是不可接受的,因为 Button
class 和一大堆其他 classes.[=34= 支持的所有方法都需要一个访问方法。 ]
有人可以告诉我如何修改这些解决方案或者建议其他一些在这种情况下更有意义的解决方案吗?提前致谢。
编辑 基于 od @ruakh 的回答
所以 pimpl 解决方案看起来像这样:
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
这个问题是在 Button
的构造器中调用 Base(bi)
是行不通的,因为 buttonimpl
不继承 baseimpl
,它只是子class FrameWorkABase
.
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()
.
我可以想到三种方法来解决这个问题:
- 消除 Base::pimpl_ 以支持纯虚拟保护函数 Base::pimpl_()。让 subclasses 实现该函数以提供指向 Base::show 的实现指针(以及任何其他需要它的 base-class 函数)。
- 使 Base::pimpl_ 私有而不是受保护,并为子class 提供它们自己的实现指针的适当类型的副本。 (因为 subclasses 负责调用 base-class 构造函数,所以他们可以确保给它提供与他们计划使用的相同的实现指针。)
- 使 Base::show 成为纯虚函数(以及任何其他基 class 函数),并在 subclasses 中实现它。如果这导致代码重复,请创建一个子classes 可以使用的单独辅助函数。
我认为#3 是最好的方法,因为它避免了将您的 class 层次结构耦合到基础框架的 class 层次结构;但我从你上面的评论中怀疑你会不同意。没关系。
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.
无论您采用何种方法,如果您不希望客户端代码必须设置实现指针(因为这意味着与底层框架交互),那么您将需要提供构造函数或工厂方法来执行所以。由于您希望通过客户端代码支持继承,这意味着提供处理此问题的构造函数。所以我认为您过快地取消了 Pimpl 习语。
关于您的编辑 — 而不是让 Base::impl 和 Button::impl 扩展 FrameworkABase 和 FrameworkAButton,您应该使 FrameworkAButton 成为 Button::impl 的数据成员,并给 Base::impl 一个指向它的指针。 (或者你可以给 Button::impl 一个 std::unique_ptr 到 FrameworkAButton 而不是直接持有它;这使得以明确定义的方式将指针传递给 Base::impl 更容易一些。)
例如:
#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;
}
我有一项任务是创建一种多平台 C++ GUI 库。它在不同的平台上封装了不同的 GUI 框架。该库本身提供了一个界面,用户可以通过该界面统一通信,而不管他使用的是什么平台。
我需要正确设计此接口以及与框架的底层通信。我试过的是:
- Pimpl 惯用法 - 最初选择此解决方案是因为它的优点 - 二进制兼容性、削减依赖树以增加构建时间...
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
但是,据我了解,此模式并不是为您可以轻松扩展接口对象及其实现的复杂层次结构而设计的。例如。如果用户想要从库 UserButton : Button
中子 class 按钮,他需要知道 pimpl 习语模式的细节才能正确初始化实现。
- 简单的实现指针——用户不需要知道库的底层设计——如果他想创建一个自定义控件,他只需简单地subclasses库控件,剩下的就搞定了来自图书馆
#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
}
};
由于实现受到保护,相同的 implptr
对象将用于 Button
- 这是可能的,因为 FrameWorkAButton
和 FrameWorkBButton
都继承自 FrameWorkABBase
和 FrameWorkABase
分别。这个解决方案的问题是每次我需要打电话给在 Button
class 类似 pimpl_->click()
中,我需要向下转换 pimpl_
,因为 clickA()
方法不在 FrameWorkABase
中,而是在 FrameWorkAButton
,所以它看起来像这样 (static_cast<FrameWorkAButton>(pimpl_))->click()
。过度向下转型是糟糕设计的标志。在这种情况下,访问者模式是不可接受的,因为 Button
class 和一大堆其他 classes.[=34= 支持的所有方法都需要一个访问方法。 ]
有人可以告诉我如何修改这些解决方案或者建议其他一些在这种情况下更有意义的解决方案吗?提前致谢。
编辑 基于 od @ruakh 的回答
所以 pimpl 解决方案看起来像这样:
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
这个问题是在 Button
的构造器中调用 Base(bi)
是行不通的,因为 buttonimpl
不继承 baseimpl
,它只是子class FrameWorkABase
.
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()
.
我可以想到三种方法来解决这个问题:
- 消除 Base::pimpl_ 以支持纯虚拟保护函数 Base::pimpl_()。让 subclasses 实现该函数以提供指向 Base::show 的实现指针(以及任何其他需要它的 base-class 函数)。
- 使 Base::pimpl_ 私有而不是受保护,并为子class 提供它们自己的实现指针的适当类型的副本。 (因为 subclasses 负责调用 base-class 构造函数,所以他们可以确保给它提供与他们计划使用的相同的实现指针。)
- 使 Base::show 成为纯虚函数(以及任何其他基 class 函数),并在 subclasses 中实现它。如果这导致代码重复,请创建一个子classes 可以使用的单独辅助函数。
我认为#3 是最好的方法,因为它避免了将您的 class 层次结构耦合到基础框架的 class 层次结构;但我从你上面的评论中怀疑你会不同意。没关系。
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.
无论您采用何种方法,如果您不希望客户端代码必须设置实现指针(因为这意味着与底层框架交互),那么您将需要提供构造函数或工厂方法来执行所以。由于您希望通过客户端代码支持继承,这意味着提供处理此问题的构造函数。所以我认为您过快地取消了 Pimpl 习语。
关于您的编辑 — 而不是让 Base::impl 和 Button::impl 扩展 FrameworkABase 和 FrameworkAButton,您应该使 FrameworkAButton 成为 Button::impl 的数据成员,并给 Base::impl 一个指向它的指针。 (或者你可以给 Button::impl 一个 std::unique_ptr 到 FrameworkAButton 而不是直接持有它;这使得以明确定义的方式将指针传递给 Base::impl 更容易一些。)
例如:
#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;
}