【问题标题】:How to avoid downcasting in this specific class hierarchy design?在这种特定的类层次结构设计中如何避免向下转换?
【发布时间】:2020-06-30 08:08:44
【问题描述】:

我的任务是创建一种多平台 C++ GUI 库。它在不同平台上封装了不同的 GUI 框架。库本身提供了一个接口,用户可以通过该接口进行统一通信,而不管他使用的是什么平台。

我需要正确设计这个接口和与框架的底层通信。我试过的是:

  1. Pimpl idiom - 最初选择此解决方案是因为它的优点 - 二进制兼容性、削减依赖关系树以增加构建时间...
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 中继承按钮,他需要知道 pimpl idiom 模式的细节才能正确初始化实现。

  1. 简单的实现指针 - 用户不需要知道库的底层设计 - 如果他想创建自定义控件,他只需将库控件子类化,其余的由库处理
#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
    }
};

由于实现受到保护,Button 中将使用相同的 implptr 对象 - 这是可能的,因为 FrameWorkAButtonFrameWorkBButton 分别继承自 FrameWorkABBaseFrameWorkABase。这个解决方案的问题是每次我需要打电话时,例如在Button 类中类似于pimpl_-&gt;click(),我需要向下转换pimpl_,因为clickA() 方法不在FrameWorkABase 中,而是在FrameWorkAButton 中,所以它看起来像这样(static_cast&lt;FrameWorkAButton&gt;(pimpl_))-&gt;click()。过度沮丧是糟糕设计的标志。在这种情况下,访问者模式是不可接受的,因为Button 类和一大堆其他类支持的所有方法都需要一个访问方法。

有人可以告诉我,如何修改这些解决方案,或者提出其他解决方案,这在这种情况下会更有意义吗?提前致谢。

EDIT 基于 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 的ctor 中调用Base(bi) 将不起作用,因为buttonimpl 不继承baseimpl,只是它的子类FrameWorkABase

【问题讨论】:

  • 您不能在同一个程序中有两个不同的 Base::impl 定义(或者,在您的第二种方法中,impl)。根据您的操作方式,结果要么是可诊断的错误(如果两个定义在同一个编译单元中可见)或未定义的行为(如果定义在不同的编译单元中)。
  • 您似乎正在使用继承来处理它并不真正需要的事情。例如,为什么需要FrameWorkABase 基类?
  • @ruakh 表示 GUI 控制的基础对象 - 例如在 Qt 中它是 QWidget。然后,您可以定义所有控件通用的方法,如 show() 或 getPosition()。
  • 看Base中的这段代码:impl* pimpl_;在您的代码知道 FrameWorkAButton 的地方,为什么不使用它来代替:FrameWorkAButton* pimpl_; ?
  • @ruakh 根据您的回答,FrameWorkABase 是一个通用的 GUI 控件对象。如果是这种情况,我猜每个控件对象都是可点击的,这意味着 FrameWorkABase 类应该有一个虚拟的click() 函数,并且每个子类都应该实现自己的点击方法。这样您就可以使用多态性来调用特定行为而无需向下转换(在这种情况下,这是一个糟糕的设计,因为它会产生依赖关系)

标签: c++ oop design-patterns polymorphism pimpl-idiom


【解决方案1】:

这个解决方案的问题是每次我需要打电话给例如在Button 类中类似于pimpl_-&gt;click(),我需要向下转换pimpl_,因为clickA() 方法不在FrameWorkABase 中,而是在FrameWorkAButton 中,所以它看起来像这样(static_cast&lt;FrameWorkAButton&gt;(pimpl_))-&gt;click()

我能想到三种方法来解决这个问题:

  1. 消除 Base::pimpl_ 以支持纯虚拟保护函数 Base::pimpl_()。让子类实现该函数以提供指向 Base::show(以及任何其他需要它的基类函数)的实现指针。
  2. 将 Base::pimpl_ 设为私有而不是受保护,并为子类提供它们自己的适当类型的实现指针副本。 (由于子类负责调用基类构造函数,它们可以确保为它提供与计划使用相同的实现指针。)
  3. 使 Base::show 成为纯虚函数(以及任何其他基类函数),并在子类中实现它。如果这导致代码重复,请创建一个子类可以使用的单独帮助函数。

我认为#3 是最好的方法,因为它避免了将你的类层次结构耦合到底层框架的类层次结构;但我怀疑你上面的cmets你会不同意。没关系。


例如如果用户想从库 UserButton : Button 中继承按钮,他需要知道 pimpl idiom 模式的细节才能正确初始化实现。

无论您采用何种方法,如果您不希望客户端代码必须设置实现指针(因为这意味着与底层框架交互),那么您将需要提供构造函数或工厂方法来执行此操作。由于您希望通过客户端代码支持继承,这意味着提供处理此问题的构造函数。所以我认为你写得太快了 Pimpl 成语。


关于您的编辑 - 而不是 Base::impl 和 Button::impl extend 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;
}

【讨论】:

  • 感谢您的回复。至于第一点-ofc 这将最小化向下转换计数,但向下转换本身无论如何都需要在 pimpl() 方法中完成。第二点让我想起了 pimpl idiom 解决方案,我最喜欢这三点,因为第三点并不完全符合 OOP。关于工厂和构造函数的评论让我思考,我认为这是要走的路。但是,我需要 impl 类的前向声明才能在外部创建其对象,例如Button 上课不是吗? (不是问题,只是让人放心)
  • 而“前向声明”是指在类外声明(而不是作为私有字段)——以便可以在工厂中实例化该类。此外,所有私有变量都需要从 BaseButton 类到它们的实现(这将创建重复的数据 - 例如 Base 具有私有字段“事件处理程序”,这是由触发的用户定义函数的映射像按钮单击这样的事件 - 这需要在每个框架的每个 Base::impl 类中声明)。
  • 此外,我猜不可能有Base::impl,因为Button::impl 将不再继承Base 的实现,因为Base::impl : FrameworkABaseButton::impl : FrameworkAButtonFrameworkAButton : FrameworkABase .因此我无法将Button::impl 传递给Base 的构造函数并将其向下转换为Button::impl。我建议,Base 不会实现 pimpl 成语,它会像我的问题 2. 解决方案中那样具有基本指针,但另一方面,Base 的子类将实现 pimpl 成语。
  • 回复:“至于第一点 - ofc 这将最大限度地减少向下转换的计数,但向下转换本身需要在 pimpl() 方法中完成”:一点也不;我不知道你为什么这么说。由于 pimpl 字段在子类中,因此它具有正确的开头类型;不需要向下转换。
  • 啊,是的,你是对的——由于返回类型是协变的。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-12-29
  • 2016-03-11
  • 1970-01-01
  • 2013-06-24
相关资源
最近更新 更多