c++ - 如何使用 Qt 的 PIMPL 成语?

标签 c++ qt pimpl-idiom

PIMPL 代表 P ointer 到 IMPL mentation。实现代表“实现细节”:类的用户不需要关心的东西。

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

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

当我们有一个半复杂的实现时,使用 PIMPL 的动机就变得很明显了。进一步的动机在 this question 中给出。即使是一个相当简单的类也必须在其接口(interface)中引入许多其他头文件。

dialog screenshot

基于 PIMPL 的界面相当干净和可读。

// 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)

基于 Qt 5、C++11 的接口(interface)不需要 Q_PRIVATE_SLOT 行。

将其与将实现细节放入接口(interface)私有(private)部分的非 PIMPL 接口(interface)进行比较。请注意必须包含多少其他代码。
// 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)

就公共(public)接口(interface)而言,这两个接口(interface)完全等效。它们具有相同的信号、槽和公共(public)方法。

最佳答案

介绍
PIMPL 是一个私有(private)类,它包含父类的所有特定于实现的数据。 Qt 提供了一个 PIMPL 框架和一组使用该框架时需要遵循的约定。 Qt 的 PIMPL 可用于所有类,甚至那些不是从 QObject 派生的类。
PIMPL 需要在堆上分配。在惯用的 C++ 中,我们不能手动管理这样的存储,而是使用智能指针。 QScopedPointerstd::unique_ptr 均可用于此目的。因此,一个最小的基于 pimpl 的接口(interface),不是从 QObject 派生的,可能看起来像:

// Foo.h
#include <QScopedPointer>
class FooPrivate; ///< The PIMPL class for Foo
class Foo {
  QScopedPointer<FooPrivate> const d_ptr;
public:
  Foo();
  ~Foo();
};
析构函数的声明是必要的,因为作用域指针的析构函数需要析构 PIMPL 的一个实例。析构函数必须在 FooPrivate 类所在的实现文件中生成:
// Foo.cpp
class FooPrivate { };
Foo::Foo() : d_ptr(new FooPrivate) {}
Foo::~Foo() {}
也可以看看:
  • A deeper exposition of the idiom .
  • Gotchas and pitfalls of PIMPL .

  • 界面
    我们现在将解释问题中基于 PIMPL 的 CoordinateDialog 接口(interface)。
    Qt 提供了几个宏和实现助手来减少 PIMPL 的苦差事。实现要求我们遵循以下规则:
  • Foo 的 PIMPL 被命名为 FooPrivate
  • PIMPL 在接口(interface)(头文件)文件中与 Foo 类的声明一起前向声明。

  • Q_DECLARE_PRIVATE 宏Q_DECLARE_PRIVATE 宏必须放在类声明的 private 部分。它将接口(interface)类的名称作为参数。它声明了 d_func() 辅助方法的两个内联实现。该方法返回具有适当常量的 PIMPL 指针。在 const 方法中使用时,它返回一个指向 const PIMPL 的指针。在非常量方法中,它返回一个指向非常量 PIMPL 的指针。它还在派生类中提供了正确类型的 pimpl。因此,从实现内部对 pimpl 的所有访问都将使用 d_func() 和 ** 而不是通过 d_ptr 完成。通常我们会使用 Q_D 宏,如下面的实现部分所述。
    宏有两种形式:
    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
    
    在我们的例子中, Q_DECLARE_PRIVATE(CoordinateDialog) 相当于 Q_DECLARE_PRIVATE_D(d_ptr, CoordinateDialog)
    Q_PRIVATE_SLOT 宏
    该宏仅在与 Qt 4 兼容或面向非 C++11 编译器时才需要。对于 Qt 5、C++11 代码,这是不必要的,因为我们可以将仿函数连接到信号,并且不需要显式的私有(private)槽。
    我们有时需要一个 QObject 来拥有供内部使用的私有(private)插槽。这样的插槽会污染接口(interface)的私有(private)部分。由于插槽信息仅与 moc 代码生成器相关,因此我们可以使用 Q_PRIVATE_SLOT 宏来告诉 moc 通过 d_func() 指针而不是通过 this 来调用给定的插槽。Q_PRIVATE_SLOT 中的 moc 预期的语法是:
    Q_PRIVATE_SLOT(instance_pointer, method signature)
    
    在我们的例子中:
    Q_PRIVATE_SLOT(d_func(), void onAccepted())
    
    这有效地在 onAccepted 类上声明了一个 CoordinateDialog 插槽。 moc 生成以下代码来调用插槽:
    d_func()->onAccepted()
    
    宏本身有一个空扩展——它只向​​ moc 提供信息。
    我们的接口(interface)类因此扩展如下:
    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:
      [...]
    };
    
    使用此宏时,必须在完全定义私有(private)类的地方包含 moc 生成的代码。在我们的例子中,这意味着 CoordinateDialog.cpp 文件应该在 结束 并带有:
    #include "moc_CoordinateDialog.cpp"
    
    陷阱
  • 在类声明中使用的所有 Q_ 宏都已经包含一个分号。 Q_ 之后不需要明确的分号:
      // correct                       // verbose, has double semicolons
      class Foo : public QObject {     class Foo : public QObject {
        Q_OBJECT                         Q_OBJECT;
        Q_DECLARE_PRIVATE(...)           Q_DECLARE_PRIVATE(...);
        ...                              ...
      };                               };
    
  • PIMPL 不能是 Foo 本身内的私有(private)类:
      // correct                  // wrong
      class FooPrivate;           class Foo {
      class Foo {                   class FooPrivate;
        ...                         ...
      };                          };
    
  • 默认情况下,类声明中左大括号之后的第一部分是私有(private)的。因此,以下内容是等效的:
      // less wordy, preferred    // verbose
      class Foo {                 class Foo {              
        int privateMember;        private:
                                    int privateMember;
      };                          };
    
  • Q_DECLARE_PRIVATE 需要接口(interface)类的名称,而不是 PIMPL 的名称:
      // correct                  // wrong
      class Foo {                 class Foo {
        Q_DECLARE_PRIVATE(Foo)      Q_DECLARE_PRIVATE(FooPrivate)
        ...                         ...
      };                          };
    
  • 对于不可复制/不可分配的类,例如 QObject,PIMPL 指针应该是常量。在实现可复制类时,它可以是非常量的。
  • 由于 PIMPL 是一个内部实现细节,它的大小在使用该接口(interface)的站点上是不可用的。应该抵制使用placement new 和Fast Pimpl 惯用语的诱惑,因为除了根本不分配内存的类之外,它对任何东西都没有好处。

  • 实现
    PIMPL 必须在实现文件中定义。如果它很大,它也可以定义在一个私有(private)头中,对于接口(interface)在 foo_p.h 中的类,通常命名为 foo.h
    PIMPL 至少只是主类数据的载体。它只需要一个构造函数,不需要其他方法。在我们的例子中,它还需要存储指向主类的指针,因为我们希望从主类发出信号。因此:
    // 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*);
    };
    
    PIMPL 不可复制。由于我们使用不可复制的成员,任何复制或分配给 PIMPL 的尝试都会被编译器捕获。通常,最好使用 Q_DISABLE_COPY 显式禁用复制功能。Q_DECLARE_PUBLIC 宏的工作原理与 Q_DECLARE_PRIVATE 类似。本节稍后将对其进行描述。
    我们将指向对话框的指针传递给构造函数,允许我们初始化对话框上的布局。我们还将 QDialog 的接受信号连接到内部 onAccepted 插槽。
    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
    }
    
    onAccepted() PIMPL 方法需要在 Qt 4/非 C++11 项目中作为插槽公开。对于 Qt 5 和 C++11,这不再是必要的。
    接受对话后,我们捕获坐标并发出 acceptedCoordinates 信号。这就是我们需要公共(public)指针的原因:
    void CoordinateDialogPrivate::onAccepted() {
      Q_Q(CoordinateDialog);
      coordinates.setX(x.value());
      coordinates.setY(y.value());
      coordinates.setZ(z.value());
      emit q->acceptedCoordinates(coordinates);
    }
    
    Q_Q 宏声明了一个本地 CoordinateDialog * const q 变量。本节稍后将对其进行描述。
    实现的公共(public)部分构造 PIMPL 并公开其属性:
    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() {}
    
    Q_D 宏声明了一个本地 CoordinateDialogPrivate * const d 变量。下面描述。
    Q_D 宏
    要在接口(interface)方法中访问 PIMPL,我们可以使用 Q_D 宏,将接口(interface)类的名称传递给它。
    void Class::foo() /* non-const */ {
      Q_D(Class);    /* needs a semicolon! */
      // expands to
      ClassPrivate * const d = d_func();
      ...
    
    要在 const 接口(interface)方法中访问 PIMPL,我们需要在类名前加上 const 关键字:
    void Class::bar() const {
      Q_D(const Class);
      // expands to
      const ClassPrivate * const d = d_func();
      ...
    
    Q_Q 宏
    要从非常量 PIMPL 方法访问接口(interface)实例,我们可以使用 Q_Q 宏,将接口(interface)类的名称传递给它。
    void ClassPrivate::foo() /* non-const*/ {
      Q_Q(Class);   /* needs a semicolon! */
      // expands to
      Class * const q = q_func();
      ...
    
    要在 const PIMPL 方法中访问接口(interface)实例,我们在类名前加上 const 关键字,就像我们对 Q_D 宏所做的一样:
    void ClassPrivate::foo() const {
      Q_Q(const Class);   /* needs a semicolon! */
      // expands to
      const Class * const q = q_func();
      ...
    
    Q_DECLARE_PUBLIC 宏
    该宏是可选的,用于允许从 PIMPL 访问接口(interface)。如果 PIMPL 的方法需要操作接口(interface)的基类或发出其信号,则通常使用它。等效的 Q_DECLARE_PRIVATE 宏用于允许从接口(interface)访问 PIMPL。
    该宏将接口(interface)类的名称作为参数。它声明了 q_func() 辅助方法的两个内联实现。该方法返回具有适当常量的接口(interface)指针。在 const 方法中使用时,它返回一个指向 const 接口(interface)的指针。在非常量方法中,它返回一个指向非常量接口(interface)的指针。它还在派生类中提供正确类型的接口(interface)。因此,从 PIMPL 内部对接口(interface)的所有访问都将使用 q_func() 和 ** 而不是通过 q_ptr 完成。通常我们会使用 Q_Q 宏,如上所述。
    宏期望指向接口(interface)的指针命名为 q_ptr 。这个宏没有允许为接口(interface)指针选择不同名称的两个参数变体(如 Q_DECLARE_PRIVATE 的情况)。
    宏展开如下:
    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;
      ...
    };
    
    Q_DISABLE_COPY 宏
    此宏删除复制构造函数和赋值运算符。它必须出现在 PIMPL 的私有(private)部分。
    常见问题
  • 给定类的接口(interface)头必须是包含在实现文件中的第一个头。这强制 header 是自包含的,而不依赖于碰巧包含在实现中的声明。如果不是这样,实现将无法编译,允许您修复接口(interface)以使其自给自足。
      // 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.
    
  • Q_DISABLE_COPY 宏必须出现在 PIMPL 的私有(private)部分
      // correct                    // wrong
      // Foo.cpp                    // Foo.cpp
    
      class FooPrivate {            class FooPrivate {
        Q_DISABLE_COPY(FooPrivate)  public:
        ...                           Q_DISABLE_COPY(FooPrivate)
      };                               ...
                                    };
    

  • PIMPL 和非 QObject 可复制类
    PIMPL 习惯用法允许实现可复制、可复制和可移动构造、可分配的对象。赋值是通过 copy-and-swap 惯用语完成的,防止代码重复。 PIMPL 指针当然不能是常量。
    在 C++11 中,我们需要注意 Rule of Four ,并提供以下所有内容:复制构造函数、移动构造函数、赋值运算符和析构函数。当然还有独立的 swap 函数来实现这一切†。
    我们将使用一个相当无用但仍然正确的例子来说明这一点。
    界面
    // 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();
    };
    
    为了性能,移动构造函数和赋值运算符应该在接口(interface)(头)文件中定义。他们不需要直接访问 PIMPL:
    Integer::Integer(Integer && other) : Integer() {
       swap(*this, other);
    }
    
    Integer & Integer::operator=(Integer other) {
       swap(*this, other);
       return *this;
    }
    
    所有这些都使用 swap 独立函数,我们也必须在接口(interface)中定义它。请注意,它是
    void swap(Integer& first, Integer& second) /* nothrow */ {
       using std::swap;
       swap(first.d_ptr, second.d_ptr);
    }
    
    执行
    这是比较简单的。我们不需要从 PIMPL 访问接口(interface),因此 Q_DECLARE_PUBLICq_ptr 不存在。
    // 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 :还有其他声明,我们应该为我们的类型专门化 std::swap,提供一个类内 swap 和一个自由函数 0x2518122413 的任何非必要的 0x2518122413。但是 234312413 的任何 234312413 都是不必要的,234312413 的任何 1343131313 都是不必要的。调用,我们的函数将通过 ADL 找到。一个功能就行。

    关于c++ - 如何使用 Qt 的 PIMPL 成语?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/25250171/

    相关文章:

    qt - 如何使 Qt 中的选项卡小部件的内容自动调整大小?

    c++ - 如何获取 C++ 中抽象(?)pimpl 的调试信息?

    c++ - 引用基类的 constexpr 构造函数因编译器而异

    c++ - 为什么将 '*' 放在返回结构指针的函数上?

    javascript - 如何在 PPAPI 中将变量从 JavaScript 传递到 C++?

    c++ - QT QSqlTableModel - 给定数据列中的背景颜色

    c++ - 如何将 QStandardItem 附加到两行中

    c++ - 如何衡量 pimpl 候选人?

    C++ const 正确性漏洞或意外使用?

    c++ - 一个中断函数来修改动态确定的实例?