【问题标题】:Design Issue with QtQt 的设计问题
【发布时间】:2013-02-03 17:54:44
【问题描述】:

使用 Qt 5.0.0

以下大致是一个Observer 模式(代码被剥离到最低限度,仅用于解释问题):

class A : public QObject
{
    Q_OBJECT
    public:
        void registerListner(Observer *pObs);
        static A* getInstance();
    signals:
        void sig();
};

void A::registerListner(Observer *pObs)
{
    connect(this, SIGNAL(sig()), pObs, SLOT(slo));
}

////////////////////////////////////////////////////////////////

class Observer : public QObject
{
    Q_OBJECT
    public slots:
        virtual void slo() = 0;
};

class ConcreteObserver : public Observer , public QWidget
{
    Q_OBJECT
    public: //re-mentioning "slots" is not necessary
        virtual void slo();
};

ConcreteObserver *pCObs = new ConcreteObserver;
A::getInstance()->registerListner(pCObs);

/////////////////////////////////////////////////////////////

问题(除了可怕的钻石): 不能从 QObject 继承多次 - moc 不允许。 一种可能的解决方案是从 QWidget 派生 Observer,然后仅从 Observer 派生 ConcreteObserver。然而,这对 ConcreteObserver 施加了限制。也许 ConcreteObserver_2 需要从 QDialog 派生等等。

我该如何解决这个设计问题? Qt 5.0.0 Signal-Slot 有什么特定的东西(除了早期版本)可以解决这个问题,或者你有什么建议?

【问题讨论】:

  • 你为什么需要那个 Observer 类?
  • 观察者模式 :) 那是选定的设计。在这里可能看起来并不明显,但如果一个人经历了整个项目需求,就会有意义。这只是显示问题所在的最低限度
  • 在上面的示例中,您不需要它。您可以将QObject* 传递给AregisterListener
  • @Mat 但QObject 不包含用户定义的插槽slo。它将如何工作?此外,我想通过设计设置一个约束,即所有注册的类都应该重新实现插槽slo。或者也许我没有完全理解你 - 在这种情况下进一步解释自己
  • 如果您将参数更改为QObject*,您的代码将正常编译,但如果该特定 QObject 未实现该插槽,您将在运行时收到警告。 (程序不会失败,只是不会连接任何东西。)如果需要,您也可以进行运行时检查。

标签: c++ qt observer-pattern qt5


【解决方案1】:

如果运行时警告对您来说还不够,您可以通过将 registerListener 设为函数模板来添加一些编译时类型检查,并通过不定义 Observer 本身的类来避免 QObject 的多重继承.

这可能是这样的:(注意:我的 SFINAE 技能不存在,这可能会变得更好。)

#include <QObject>
#include <QDebug>

#include <type_traits>

class A : public QObject
{
    Q_OBJECT
    public:
        template <typename T>
        void registerListener(T *pObs)
        {
            static_assert(std::is_base_of<QObject, T>::value,
                          "Listener must be a QObject");
            static_assert(std::is_same<void, 
                                       decltype(std::declval<T>().slo())
                                      >::value,
                          "Slot slo must have signature void slo();");
            connect(this, SIGNAL(sig()), pObs, SLOT(slo()));
        }
        static A* getInstance() { return instance; }
        static void init() { instance = new A; }
        void doStuff() { emit sig(); }
    signals:
        void sig();
    private:
        static A *instance;
};

几个测试用例:

class BadObject1 : public QObject
{
    Q_OBJECT
    public:
        BadObject1() {}
    public slots:
        void slo(int){}
};
class BadObject2 : public QObject
{
    Q_OBJECT
    public:
        BadObject2() {}
    public slots:
        int slo(){return 0;}
};
struct BadObject3 {
    void slo();
};

class ObservedObject : public QObject
{
    Q_OBJECT
    public:
        ObservedObject(QString const& name): QObject() {
            setObjectName(name);
        }
    public slots:
        virtual void slo(){
            qDebug() << objectName();
        }
};
class ObservedObject2 : public ObservedObject
{
    Q_OBJECT
    public:
        ObservedObject2(QString const& name)
          : ObservedObject(name + " (derived)") {}
};

还有一个主文件:

#include "A.h"

A* A::instance = 0;

int main(int , char **)
{
    A::init();
    A::getInstance()->registerListener(new BadObject1);
    A::getInstance()->registerListener(new BadObject2);
    A::getInstance()->registerListener(new BadObject3);
    A::getInstance()->registerListener(new ObservedObject("foo"));
    A::getInstance()->registerListener(new ObservedObject2("bar"));
    A::getInstance()->doStuff();
}

对于所有 BadObjectN 情况,您都会收到编译器错误。如果您将它们注释掉,输出将如下所示:

"foo" 
"bar (derived)" 

一个警告:这将检查void slo(); 成员是否确实是一个插槽。您可以在运行时通过以下方式检查:

if (pObs->metaObject()->indexOfSlot("slo()") == -1) {
    qDebug() << "Class" << pObs->metaObject()->className() 
                        << "doesn't have a slo slot.";
   ::exit(1);
}

这将起作用并执行预期的操作(除非您有一个未将插槽声明为虚拟的类层次结构 - 然后在省略 slots“说明符”的派生类中会发生奇怪的事情。所以我提倡您的文档没有您在上面对该说明符的评论:在重载插槽时拥有它总是一个好主意)。

我不相信最后的检查在编译时是可以实现的,“槽解析”是通过 QObject 元数据的运行时遍历完成的,并且涉及解析 moc 生成的字符串。即使它使用了一些递归模板魔法,我也不认为这是工作的努力。您将在注册类型处收到运行时错误消息,您可以在其中包含错误对象的实际类名。这是一个非常准确的错误消息,应该被最简单的测试用例捕获。

【讨论】:

  • :) 你对 c++11 有很多了解。我不能立即这样做,因为我缺乏对新功能的了解,而且团队中的其他开发人员也可能有问题。它也不优雅。我将坚持对 connect(...) 的返回值进行运行时断言。如果您编辑您的此答案以包含该答案(来自您上面的一个 cmets),那么我会将其标记为已接受。感谢您的洞察力。应该很有用。
  • 您在上面的最后一段代码中有一个运行时“断言”。您可以删除所有模板和 static_assert 的东西,只使用 QObject* 作为参数,唯一不会从断言中得到的是检查传递的 QObject 是否具有 slo() 方法。
  • 顺便说一句:如果您没有 C++11,您可能可以使用 boost 进行这些编译时检查。
猜你喜欢
  • 2010-10-14
  • 2017-09-24
  • 1970-01-01
  • 1970-01-01
  • 2016-01-22
  • 1970-01-01
  • 2018-11-03
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多