如何使用 Qt 的 PIMPL 成语? [英] How to use the Qt's PIMPL idiom?

查看:32
本文介绍了如何使用 Qt 的 PIMPL 成语?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

PIMPL 代表 Pointer 到 IMPLmentation.实现代表实现细节":类的用户不需要关心的东西.

Qt 自己的类实现通过使用 PIMPL 习语将接口与实现清晰地分开.然而,Qt 提供的机制没有记录.如何使用它们?

我希望这是 Qt 中关于我如何 PIMPL"的规范问题.答案是由一个简单的坐标输入对话框界面激发的,如下所示.

当我们有一个半复杂的实现时,使用 PIMPL 的动机就变得很明显了..

  • PIMPL 的陷阱和陷阱.
  • 界面

    我们现在将在问题中解释基于 PIMPL 的 CoordinateDialog 接口.

    Qt 提供了几个宏和实现助手,可以减少 PIMPL 的苦差事.实现要求我们遵循以下规则:

    • Foo 类的 PIMPL 被命名为 FooPrivate.
    • PIMPL 在接口(头文件)文件中与 Foo 类的声明一起前向声明.

    Q_DECLARE_PRIVATE 宏

    Q_DECLARE_PRIVATE 宏必须放在类声明的 private 部分.它将接口类的名称作为参数.它声明了 d_func() 辅助方法的两个内联实现.该方法返回具有适当常量的 PIMPL 指针.在 const 方法中使用时,它返回一个指向 const PIMPL 的指针.在非常量方法中,它返回一个指向非常量 PIMPL 的指针.它还在派生类中提供了正确类型的 pimpl.因此,从实现内部对 pimpl 的所有访问都将使用 d_func() 和 ** 而不是通过 d_ptr 完成.通常我们会使用 Q_D 宏,如下面的实现部分所述.

    宏有两种形式:

    Q_DECLARE_PRIVATE(Class)//假设 PIMPL 指针命名为 d_ptrQ_DECLARE_PRIVATE_D(Dptr, Class)//显式采用 PIMPL 指针名称

    在我们的例子中,Q_DECLARE_PRIVATE(CoordinateDialog) 等价于 Q_DECLARE_PRIVATE_D(d_ptr, CoordinateDialog).

    Q_PRIVATE_SLOT 宏

    此宏仅在与 Qt 4 兼容或面向非 C++11 编译器时才需要.对于 Qt 5、C++11 代码,这是不必要的,因为我们可以将函子连接到信号并且不需要显式私有槽.

    我们有时需要 QObject 来拥有供内部使用的私有插槽.这样的插槽会污染接口的私有部分.由于插槽信息仅与 moc 代码生成器相关,因此我们可以使用 Q_PRIVATE_SLOT 宏来告诉 moc 通过 d_func() 指针,而不是通过 this.

    Q_PRIVATE_SLOT 中 moc 预期的语法是:

    Q_PRIVATE_SLOT(instance_pointer, 方法签名)

    在我们的例子中:

    Q_PRIVATE_SLOT(d_func(), void onAccepted())

    这有效地声明了 CoordinateDialog 类上的 onAccepted 槽.moc 生成以下代码来调用插槽:

    d_func()->onAccepted()

    宏本身有一个空扩展 - 它只向 moc 提供信息.

    我们的接口类因此扩展如下:

    class CoordinateDialog : public QDialog{Q_OBJECT/* 我们不在这里展开它,因为它是题外话.*///Q_DECLARE_PRIVATE(CoordinateDialog)内联 CoordinateDialogPrivate* d_func() {return reinterpret_cast(qGetPtrHelper(d_ptr));}内联 const CoordinateDialogPrivate* d_func() const {return reinterpret_cast(qGetPtrHelper(d_ptr));}朋友类 CoordinateDialogPrivate;//Q_PRIVATE_SLOT(d_func(), void onAccepted())//(空的)QScopedPointerconst d_ptr;民众:[...]};

    使用这个宏时,必须在私有类完全定义的地方包含moc生成的代码.在我们的例子中,这意味着 CoordinateDialog.cpp 文件应该结束:

    #include "moc_CoordinateDialog.cpp";

    问题

    • 要在类声明中使用的所有 Q_ 宏都已包含分号.Q_ 后不需要明确的分号:

      //正确//冗长,有双分号类 Foo : 公共 QObject { 类 Foo : 公共 QObject {Q_OBJECT Q_OBJECT;Q_DECLARE_PRIVATE(...) Q_DECLARE_PRIVATE(...);……};};

    • PIMPL 不得Foo 自身内的私有类:

      //正确//错误类 FooPrivate;类 Foo {类 Foo { 类 FooPrivate;……};};

    • 默认情况下,类声明中左大括号之后的第一部分是私有的.因此以下是等价的:

      //少啰嗦,首选//冗长类 Foo { 类 Foo {内部私人会员;私人的:内部私人会员;};};

    • Q_DECLARE_PRIVATE 需要接口类的名称,而不是 PIMPL 的名称:

      //正确//错误类 Foo { 类 Foo {Q_DECLARE_PRIVATE(Foo) Q_DECLARE_PRIVATE(FooPrivate)……};};

    • 对于不可复制/不可分配的类,例如 QObject,PIMPL 指针应该是常量.在实现可复制类时,它可以是非常量.

    • 由于 PIMPL 是一个内部实现细节,它的大小在使用该接口的站点上是不可用的.应该抵制使用placement new 和Fast Pimpl 习语的诱惑,因为它没有任何好处除了一个根本不分配内存的类.

    实施

    PIMPL 必须在实现文件中定义.如果它很大,也可以定义在一个私有头文件中,对于接口在foo.h中的类,通常命名为foo_p.h.

    PIMPL 至少只是主类数据的载体.它只需要一个构造函数,不需要其他方法.在我们的例子中,它还需要存储指向主类的指针,因为我们希望从主类发出信号.因此:

    //CordinateDialog.cpp#include #include #include 类 CoordinateDialogPrivate {Q_DISABLE_COPY(CoordinateDialogPrivate)Q_DECLARE_PUBLIC(坐标对话框)CoordinateDialog * const q_ptr;QFormLayout布局;QDoubleSpinBox x, y, z;QDialogBu​​ttonBox 按钮;QVector3D 坐标;无效 onAccepted();CoordinateDialogPrivate(CoordinateDialog*);};

    PIMPL 不可复制.由于我们使用不可复制的成员,任何复制或分配给 PIMPL 的尝试都会被编译器捕获.通常,最好使用 Q_DISABLE_COPY 显式禁用复制功能.

    Q_DECLARE_PUBLIC 宏的工作方式与 Q_DECLARE_PRIVATE 类似.本节稍后将对其进行描述.

    我们将指向对话框的指针传递给构造函数,允许我们在对话框上初始化布局.我们还将 QDialog 的接受信号连接到内部 onAccepted 槽.

    CoordinateDialogPrivate::CoordinateDialogPrivate(CoordinateDialog * dialog) :q_ptr(对话框),布局(对话框),按钮(QDialogBu​​ttonBox::Ok | QDialogBu​​ttonBox::Cancel){layout.addRow(X", &x);layout.addRow(Y", &y);layout.addRow(Z", &z);layout.addRow(&buttons);dialog->connect(&buttons, SIGNAL(accepted()), SLOT(accept()));dialog->connect(&buttons, SIGNAL(rejected()), SLOT(reject()));#if QT_VERSION <= QT_VERSION_CHECK(5,0,0)this->connect(dialog, SIGNAL(accepted()), SLOT(onAccepted()));#别的QObject::connect(dialog, &QDialog::accepted, [this]{ onAccepted(); });#万一}

    onAccepted() PIMPL 方法需要作为 Qt 4/非 C++11 项目中的插槽公开.对于 Qt 5 和 C++11,这不再是必要的.

    在接受对话后,我们捕获坐标并发出acceptedCoordinates 信号.这就是为什么我们需要公共指针:

    void CoordinateDialogPrivate::onAccepted() {Q_Q(坐标对话框);坐标.setX(x.value());坐标.setY(y.value());坐标.setZ(z.value());发出 q->acceptedCoordinates(coordinates);}

    Q_Q 宏声明了一个本地 CoordinateDialog * const q 变量.本节稍后将对其进行描述.

    实现的公共部分构造 PIMPL 并公开其属性:

    CoordinateDialog::CoordinateDialog(QWidget * parent, Qt::WindowFlags flags) :QDialog(父,标志),d_ptr(新的 CoordinateDialogPrivate(this)){}QVector3D CoordinateDialog::coordinates() const {Q_D(const CoordinateDialog);返回 d-> 坐标;}CoordinateDialog::~CoordinateDialog() {}

    Q_D 宏声明了一个本地 CoordinateDialogPrivate * const d 变量.描述如下.

    Q_D 宏

    要在 interface 方法中访问 PIMPL,我们可以使用 Q_D 宏,将接口类的名称传递给它.

    void Class::foo()/* 非常量 */{Q_D(类);/* 需要一个分号!*///扩展为ClassPrivate * const d = d_func();...

    要在 const interface 方法中访问 PIMPL,我们需要在类名前加上 const 关键字:

    void Class::bar() const {Q_D(const 类);//扩展为const ClassPrivate * const d = d_func();...

    Q_Q 宏

    要从非常量 PIMPL 方法访问接口实例,我们可以使用 Q_Q 宏,将接口类的名称传递给它.

    void ClassPrivate::foo()/* 非常量*/{Q_Q(班级);/* 需要一个分号!*///扩展为类 * const q = q_func();...

    为了在 const PIMPL 方法中访问接口实例,我们在类名前加上 const 关键字,就像我们对 Q_D宏:

    void ClassPrivate::foo() const {Q_Q(const 类);/* 需要一个分号!*///扩展为const 类 * const q = q_func();...

    Q_DECLARE_PUBLIC 宏

    这个宏是可选的,用于允许从 PIMPL 访问接口.如果 PIMPL 的方法需要操作接口的基类或发出其信号,则通常使用它.等效的 Q_DECLARE_PRIVATE 宏用于允许从界面访问 PIMPL.

    宏将接口类的名称作为参数.它声明了 q_func() 辅助方法的两个内联实现.该方法返回具有适当常量的接口指针.在 const 方法中使用时,它返回一个指向 const 接口的指针.在非常量方法中,它返回一个指向非常量接口的指针.它还在派生类中提供正确类型的接口.因此,从 PIMPL 内部对接口的所有访问都将使用 q_func() 和 ** 而不是通过 q_ptr 完成.通常我们会使用 Q_Q 宏,如上所述.

    宏期望指向接口的指针命名为q_ptr.这个宏没有允许为接口指针选择不同名称的两个参数变体(如 Q_DECLARE_PRIVATE 的情况).

    宏展开如下:

    class CoordinateDialogPrivate {//Q_DECLARE_PUBLIC(CoordinateDialog)内联 CoordinateDialog* q_func() {return static_cast(q_ptr);}内联 const CoordinateDialog* q_func() const {return static_cast(q_ptr);}朋友类 CoordinateDialog;//CoordinateDialog * const q_ptr;...};

    Q_DISABLE_COPY 宏

    这个宏删除了复制构造函数和赋值运算符.它必须出现在 PIMPL 的私有部分.

    常见问题

    • 给定类的 interface 标头必须是要包含在实现文件中的第一个标头.这强制标头是自包含的,而不依赖于碰巧包含在实现中的声明.如果不是这样,实现将无法编译,允许您修复接口以使其自给自足.

      //正确//容易出错//Foo.cpp//Foo.cpp#include Foo.h"#include <SomethingElse>#include <SomethingElse>#include Foo.h"//现在是Foo.h"可以不依赖其他东西//我们意识到这个事实.

    • Q_DISABLE_COPY 宏必须出现在 PIMPL 的私有部分

      //正确//错误//Foo.cpp//Foo.cpp类 FooPrivate { 类 FooPrivate {Q_DISABLE_COPY(FooPrivate) 公开:... Q_DISABLE_COPY(FooPrivate)};...};

    PIMPL 和非 QObject 可复制类

    PIMPL 习惯用法允许实现可复制、可复制和可移动构造、可分配的对象.分配是通过 copy-and-swap 成语完成的,防止代码重复.PIMPL 指针当然不能是常量.

    在 C++11 中,我们需要注意四规则,并提供所有 以下内容:复制构造函数、移动构造函数、赋值运算符和析构函数.还有独立的 swap 函数来实现这一切,当然†.

    我们将使用一个相当无用但仍然正确的例子来说明这一点.

    界面

    //Integer.h#include <算法>#include 类IntegerPrivate;类整数{Q_DECLARE_PRIVATE(整数)QScopedPointerd_ptr;民众:整数();整数(int);整数(常量整数和其他);整数(整数&&其他);运算符 int&();运算符 int() const;整数 &运算符=(整数其他);朋友无效交换(整数和第一,整数和第二)/* nothrow */;~整数();};

    为了性能,移动构造函数和赋值运算符应该在接口(头)文件中定义.他们不需要直接访问 PIMPL:

    Integer::Integer(Integer && other) : Integer() {交换(*这个,其他);}整数 &整数::运算符=(整数其他){交换(*这个,其他);返回 *this;}

    所有这些都使用 swap 独立函数,我们也必须在接口中定义它.注意是

    void swap(Integer& first, Integer& second)/* nothrow */{使用 std::swap;交换(第一个.d_ptr,第二个.d_ptr);}

    实施

    这很简单.我们不需要从 PIMPL 访问接口,因此 Q_DECLARE_PUBLICq_ptr 不存在.

    //Integer.cpp#include Integer.h"类整数私有{民众:整数值;IntegerPrivate(int i) : value(i) {}};Integer::Integer() : d_ptr(new IntegerPrivate(0)) {}Integer::Integer(int i) : d_ptr(new IntegerPrivate(i)) {}Integer::Integer(const Integer &other) :d_ptr(new IntegerPrivate(other.d_func()->value)) {}Integer::operator int&() { return d_func()->value;}Integer::operator int() const { return d_func()->value;}整数::~整数() {}

    †Per 这个很好的答案:还有其他说法我们应该为我们的类型专门化 std::swap,提供一个类内 swap 以及一个自由函数 swap,等等.但是这都是不必要的:任何 swap 的正确使用都将通过不合格的调用,我们的函数将通过 ADL.一个功能就行.

    PIMPL stands for Pointer to IMPLementation. The implementation stands for "implementation detail": something that the users of the class need not to be concerned with.

    Qt's own class implementations cleanly separate out the interfaces from the implementations through the use of the PIMPL idiom. Yet, the mechanisms provided by Qt are undocumented. How to use them?

    I'd like this to be the canonical question about "how do I PIMPL" in Qt. The answers are to be motivated by a simple coordinate entry dialog interface shown below.

    The motivation for the use of PIMPL becomes apparent when we have anything with a semi-complex implementation. Further motivation is given in this question. Even a fairly simple class has to pull in a lot of other headers in its interface.

    The PIMPL-based interface is fairly clean and readable.

    // CoordinateDialog.h
    #include <QDialog>
    #include <QVector3D>
    
    class CoordinateDialogPrivate;
    class CoordinateDialog : public QDialog
    {
      Q_OBJECT
      Q_DECLARE_PRIVATE(CoordinateDialog)
    #if QT_VERSION <= QT_VERSION_CHECK(5,0,0)
      Q_PRIVATE_SLOT(d_func(), void onAccepted())
    #endif
      QScopedPointer<CoordinateDialogPrivate> const d_ptr;
    public:
      CoordinateDialog(QWidget * parent = 0, Qt::WindowFlags flags = 0);
      ~CoordinateDialog();
      QVector3D coordinates() const;
      Q_SIGNAL void acceptedCoordinates(const QVector3D &);
    };
    Q_DECLARE_METATYPE(QVector3D)
    

    A Qt 5, C++11 based interface doesn't need the Q_PRIVATE_SLOT line.

    Compare that to a non-PIMPL interface that tucks implementation details into the private section of the interface. Note how much other code has to be included.

    // CoordinateDialog.h
    #include <QDialog>
    #include <QVector3D>
    #include <QFormLayout>
    #include <QDoubleSpinBox>
    #include <QDialogButtonBox>
    
    class CoordinateDialog : public QDialog
    {
      QFormLayout m_layout;
      QDoubleSpinBox m_x, m_y, m_z;
      QVector3D m_coordinates;
      QDialogButtonBox m_buttons;
      Q_SLOT void onAccepted();
    public:
      CoordinateDialog(QWidget * parent = 0, Qt::WindowFlags flags = 0);
      QVector3D coordinates() const;
      Q_SIGNAL void acceptedCoordinates(const QVector3D &);
    };
    Q_DECLARE_METATYPE(QVector3D)
    

    Those two interfaces are exactly equivalent as far as their public interface is concerned. They have the same signals, slots and public methods.

    解决方案

    Introduction

    The PIMPL is a private class that contains all of the implementation-specific data of the parent class. Qt provides a PIMPL framework and a set of conventions that need to be followed when using that framework. Qt's PIMPLs can be used in all classes, even those not derived from QObject.

    The PIMPL needs to be allocated on the heap. In idiomatic C++, we must not manage such storage manually, but use a smart pointer. Either QScopedPointer or std::unique_ptr work for this purpose. Thus, a minimal pimpl-based interface, not derived from QObject, might look like:

    // Foo.h
    #include <QScopedPointer>
    class FooPrivate; ///< The PIMPL class for Foo
    class Foo {
      QScopedPointer<FooPrivate> const d_ptr;
    public:
      Foo();
      ~Foo();
    };
    

    The destructor's declaration is necessary, since the scoped pointer's destructor needs to destruct an instance of the PIMPL. The destructor must be generated in the implementation file, where the FooPrivate class lives:

    // Foo.cpp
    class FooPrivate { };
    Foo::Foo() : d_ptr(new FooPrivate) {}
    Foo::~Foo() {}
    

    See also:

    The Interface

    We'll now explain the PIMPL-based CoordinateDialog interface in the question.

    Qt provides several macros and implementation helpers that reduce the drudgery of PIMPLs. The implementation expects us to follow these rules:

    • The PIMPL for a class Foo is named FooPrivate.
    • The PIMPL is forward-declared along the declaration of the Foo class in the interface (header) file.

    The Q_DECLARE_PRIVATE Macro

    The Q_DECLARE_PRIVATE macro must be put in the private section of the class's declaration. It takes the interface class's name as a parameter. It declares two inline implementations of the d_func() helper method. That method returns the PIMPL pointer with proper constness. When used in const methods, it returns a pointer to a const PIMPL. In non-const methods, it returns a pointer to a non-const PIMPL. It also provides a pimpl of correct type in derived classes. It follows that all access to the pimpl from within the implementation is to be done using d_func() and **not through d_ptr. Usually we'd use the Q_D macro, described in the Implementation section below.

    The macro comes in two flavors:

    Q_DECLARE_PRIVATE(Class)   // assumes that the PIMPL pointer is named d_ptr
    Q_DECLARE_PRIVATE_D(Dptr, Class) // takes the PIMPL pointer name explicitly
    

    In our case, Q_DECLARE_PRIVATE(CoordinateDialog) is equivalent to Q_DECLARE_PRIVATE_D(d_ptr, CoordinateDialog).

    The Q_PRIVATE_SLOT Macro

    This macro is only needed for Qt 4 compatibility, or when targeting non-C++11 compilers. For Qt 5, C++11 code, it is unnecessary, as we can connect functors to signals and there's no need for explicit private slots.

    We sometimes need for a QObject to have private slots for internal use. Such slots would pollute the interface's private section. Since the information about slots is only relevant to the moc code generator, we can, instead, use the Q_PRIVATE_SLOT macro to tell moc that a given slot is to be invoked through the d_func() pointer, instead of through this.

    The syntax expected by moc in the Q_PRIVATE_SLOT is:

    Q_PRIVATE_SLOT(instance_pointer, method signature)
    

    In our case:

    Q_PRIVATE_SLOT(d_func(), void onAccepted())
    

    This effectively declares an onAccepted slot on the CoordinateDialog class. The moc generates the following code to invoke the slot:

    d_func()->onAccepted()
    

    The macro itself has an empty expansion - it only provides information to moc.

    Our interface class is thus expanded as follows:

    class CoordinateDialog : public QDialog
    {
      Q_OBJECT /* We don't expand it here as it's off-topic. */
      // Q_DECLARE_PRIVATE(CoordinateDialog)
      inline CoordinateDialogPrivate* d_func() { 
        return reinterpret_cast<CoordinateDialogPrivate *>(qGetPtrHelper(d_ptr));
      }
      inline const CoordinateDialogPrivate* d_func() const { 
        return reinterpret_cast<const CoordinateDialogPrivate *>(qGetPtrHelper(d_ptr));
      }
      friend class CoordinateDialogPrivate;
      // Q_PRIVATE_SLOT(d_func(), void onAccepted())
      // (empty)
      QScopedPointer<CoordinateDialogPrivate> const d_ptr;
    public:
      [...]
    };
    

    When using this macro, you must include the moc-generated code in a place where the private class is fully defined. In our case, this means that the CoordinateDialog.cpp file should end with:

    #include "moc_CoordinateDialog.cpp"
    

    Gotchas

    • All of the Q_ macros that are to be used in a class declaration already include a semicolon. No explicit semicolons are needed after Q_:

        // correct                       // verbose, has double semicolons
        class Foo : public QObject {     class Foo : public QObject {
          Q_OBJECT                         Q_OBJECT;
          Q_DECLARE_PRIVATE(...)           Q_DECLARE_PRIVATE(...);
          ...                              ...
        };                               };
      

    • The PIMPL must not be a private class within Foo itself:

        // correct                  // wrong
        class FooPrivate;           class Foo {
        class Foo {                   class FooPrivate;
          ...                         ...
        };                          };
      

    • The first section after the opening brace in a class declaration is private by default. Thus the following are equivalent:

        // less wordy, preferred    // verbose
        class Foo {                 class Foo {              
          int privateMember;        private:
                                      int privateMember;
        };                          };
      

    • The Q_DECLARE_PRIVATE expects the interface class's name, not the PIMPL's name:

        // correct                  // wrong
        class Foo {                 class Foo {
          Q_DECLARE_PRIVATE(Foo)      Q_DECLARE_PRIVATE(FooPrivate)
          ...                         ...
        };                          };
      

    • The PIMPL pointer should be const for non-copyable/non-assignable classes such as QObject. It can be non-const when implementing copyable classes.

    • Since the PIMPL is an internal implementation detail, its size is not available at the site where the interface is used. The temptation to use placement new and the Fast Pimpl idiom should be resisted as it provides no benefits for anything but a class that doesn't allocate memory at all.

    The Implementation

    The PIMPL has to be defined in the implementation file. If it is large, it can also be defined in a private header, customarily named foo_p.h for a class whose interface is in foo.h.

    The PIMPL, at a minimum, is merely a carrier of the main class's data. It only needs a constructor and no other methods. In our case, it also needs to store the pointer to the main class, as we'll want to emit a signal from the main class. Thus:

    // CordinateDialog.cpp
    #include <QFormLayout>
    #include <QDoubleSpinBox>
    #include <QDialogButtonBox>
    
    class CoordinateDialogPrivate {
      Q_DISABLE_COPY(CoordinateDialogPrivate)
      Q_DECLARE_PUBLIC(CoordinateDialog)
      CoordinateDialog * const q_ptr;
      QFormLayout layout;
      QDoubleSpinBox x, y, z;
      QDialogButtonBox buttons;
      QVector3D coordinates;
      void onAccepted();
      CoordinateDialogPrivate(CoordinateDialog*);
    };
    

    The PIMPL is not copyable. Since we use non-copyable members, any attempt to copy or assign to the PIMPL would be caught by the compiler. Generally, it's best to explicitly disable the copy functionality by using Q_DISABLE_COPY.

    The Q_DECLARE_PUBLIC macro works similarly to Q_DECLARE_PRIVATE. It is described later in this section.

    We pass the pointer to the dialog into the constructor, allowing us to initialize the layout on the dialog. We also connect the QDialog's accepted signal to the internal onAccepted slot.

    CoordinateDialogPrivate::CoordinateDialogPrivate(CoordinateDialog * dialog) :
      q_ptr(dialog),
      layout(dialog),
      buttons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel)
    {
      layout.addRow("X", &x);
      layout.addRow("Y", &y);
      layout.addRow("Z", &z);
      layout.addRow(&buttons);
      dialog->connect(&buttons, SIGNAL(accepted()), SLOT(accept()));
      dialog->connect(&buttons, SIGNAL(rejected()), SLOT(reject()));
    #if QT_VERSION <= QT_VERSION_CHECK(5,0,0)
      this->connect(dialog, SIGNAL(accepted()), SLOT(onAccepted()));
    #else
      QObject::connect(dialog, &QDialog::accepted, [this]{ onAccepted(); });
    #endif
    }
    

    The onAccepted() PIMPL method needs to be exposed as a slot in Qt 4/non-C++11 projects. For Qt 5 and C++11, this is no longer necessary.

    Upon the acceptance of the dialog, we capture the coordinates and emit the acceptedCoordinates signal. That's why we need the public pointer:

    void CoordinateDialogPrivate::onAccepted() {
      Q_Q(CoordinateDialog);
      coordinates.setX(x.value());
      coordinates.setY(y.value());
      coordinates.setZ(z.value());
      emit q->acceptedCoordinates(coordinates);
    }
    

    The Q_Q macro declares a local CoordinateDialog * const q variable. It is described later in this section.

    The public part of the implementation constructs the PIMPL and exposes its properties:

    CoordinateDialog::CoordinateDialog(QWidget * parent, Qt::WindowFlags flags) :
      QDialog(parent, flags),
      d_ptr(new CoordinateDialogPrivate(this))
    {}
    
    QVector3D CoordinateDialog::coordinates() const {
      Q_D(const CoordinateDialog);
      return d->coordinates;
    }
    
    CoordinateDialog::~CoordinateDialog() {}
    

    The Q_D macro declares a local CoordinateDialogPrivate * const d variable. It is described below.

    The Q_D Macro

    To access the PIMPL in an interface method, we can use the Q_D macro, passing it the name of the interface class.

    void Class::foo() /* non-const */ {
      Q_D(Class);    /* needs a semicolon! */
      // expands to
      ClassPrivate * const d = d_func();
      ...
    

    To access the PIMPL in a const interface method, we need to prepend the class name with the const keyword:

    void Class::bar() const {
      Q_D(const Class);
      // expands to
      const ClassPrivate * const d = d_func();
      ...
    

    The Q_Q Macro

    To access the interface instance from a non-const PIMPL method, we can use the Q_Q macro, passing it the name of the interface class.

    void ClassPrivate::foo() /* non-const*/ {
      Q_Q(Class);   /* needs a semicolon! */
      // expands to
      Class * const q = q_func();
      ...
    

    To access the interface instance in a const PIMPL method, we prepend the class name with the const keyword, just as we did for the Q_D macro:

    void ClassPrivate::foo() const {
      Q_Q(const Class);   /* needs a semicolon! */
      // expands to
      const Class * const q = q_func();
      ...
    

    The Q_DECLARE_PUBLIC Macro

    This macro is optional and is used to allow access to the interface from the PIMPL. It is typically used if the PIMPL's methods need to manipulate the interface's base class, or emit its signals. The equivalent Q_DECLARE_PRIVATE macro was used to allow access to the PIMPL from the interface.

    The macro takes the interface class's name as a parameter. It declares two inline implementations of the q_func() helper method. That method returns the interface pointer with proper constness. When used in const methods, it returns a pointer to a const interface. In non-const methods, it returns a pointer to a non-const interface. It also provides the interface of correct type in derived classes. It follows that all access to the interface from within the PIMPL is to be done using q_func() and **not through q_ptr. Usually we'd use the Q_Q macro, described above.

    The macro expects the pointer to the interface to be named q_ptr. There is no two-argument variant of this macro that would allow to choose a different name for the interface pointer (as was the case for Q_DECLARE_PRIVATE).

    The macro expands as follows:

    class CoordinateDialogPrivate {
      //Q_DECLARE_PUBLIC(CoordinateDialog)
      inline CoordinateDialog* q_func() {
        return static_cast<CoordinateDialog*>(q_ptr);
      }
      inline const CoordinateDialog* q_func() const {
        return static_cast<const CoordinateDialog*>(q_ptr);
      }
      friend class CoordinateDialog;
      //
      CoordinateDialog * const q_ptr;
      ...
    };
    

    The Q_DISABLE_COPY Macro

    This macro deletes the copy constructor and the assignment operator. It must appear in the private section of the PIMPL.

    Common Gotchas

    • The interface header for a given class must be the first header to be included in the implementation file. This forces the header to be self-contained and not dependent on declarations that happen to be included in the implementation. If it isn't so, the implementation will fail to compile, allowing you to fix the interface to make it self-sufficient.

        // correct                   // error prone
        // Foo.cpp                   // Foo.cpp
      
        #include "Foo.h"             #include <SomethingElse>
        #include <SomethingElse>     #include "Foo.h"
                                     // Now "Foo.h" can depend on SomethingElse without
                                     // us being aware of the fact.
      

    • The Q_DISABLE_COPY macro must appear in the private section of the PIMPL

        // correct                    // wrong
        // Foo.cpp                    // Foo.cpp
      
        class FooPrivate {            class FooPrivate {
          Q_DISABLE_COPY(FooPrivate)  public:
          ...                           Q_DISABLE_COPY(FooPrivate)
        };                               ...
                                      };
      

    PIMPL And Non-QObject Copyable Classes

    The PIMPL idiom allows one to implement copyable, copy- and move- constructible, assignable object. The assignment is done through the copy-and-swap idiom, preventing code duplication. The PIMPL pointer must not be const, of course.

    In C++11, we need to heed the Rule of Four, and provide all of the following: the copy constructor, move constructor, assignment operator, and destructor. And the free-standing swap function to implement it all, of course†.

    We'll illustrate this using a rather useless, but nevertheless correct example.

    Interface

    // Integer.h
    #include <algorithm>
    #include <QScopedPointer>
    
    class IntegerPrivate;
    class Integer {
       Q_DECLARE_PRIVATE(Integer)
       QScopedPointer<IntegerPrivate> d_ptr;
    public:
       Integer();
       Integer(int);
       Integer(const Integer & other);
       Integer(Integer && other);
       operator int&();
       operator int() const;
       Integer & operator=(Integer other);
       friend void swap(Integer& first, Integer& second) /* nothrow */;
       ~Integer();
    };
    

    For performance, the move constructor and the assignment operator should be defined in the interface (header) file. They don't need to access the PIMPL directly:

    Integer::Integer(Integer && other) : Integer() {
       swap(*this, other);
    }
    
    Integer & Integer::operator=(Integer other) {
       swap(*this, other);
       return *this;
    }
    

    All of those use the swap freestanding function, which we must define in the interface as well. Note that it is

    void swap(Integer& first, Integer& second) /* nothrow */ {
       using std::swap;
       swap(first.d_ptr, second.d_ptr);
    }
    

    Implementation

    This is rather straightforward. We don't need access to the interface from the PIMPL, thus Q_DECLARE_PUBLIC and q_ptr are absent.

    // Integer.cpp
    #include "Integer.h"
    
    class IntegerPrivate {
    public:
       int value;
       IntegerPrivate(int i) : value(i) {}
    };
    
    Integer::Integer() : d_ptr(new IntegerPrivate(0)) {}
    Integer::Integer(int i) : d_ptr(new IntegerPrivate(i)) {}
    Integer::Integer(const Integer &other) :
       d_ptr(new IntegerPrivate(other.d_func()->value)) {}
    Integer::operator int&() { return d_func()->value; }
    Integer::operator int() const { return d_func()->value; }
    Integer::~Integer() {}
    

    †Per this excellent answer: There are other claims that we should specialize std::swap for our type, provide an in-class swap along-side a free-function swap, etc. But this is all unnecessary: any proper use of swap will be through an unqualified call, and our function will be found through ADL. One function will do.

    这篇关于如何使用 Qt 的 PIMPL 成语?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

    查看全文
    登录 关闭
    扫码关注1秒登录
    发送“验证码”获取 | 15天全站免登陆