如何使用Qt的PIMPL习语?

时间:2014-08-11 18:39:24

标签: c++ qt pimpl-idiom

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

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

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

当我们有任何半复杂的实现时,使用PIMPL的动机变得明显。 this question给出了进一步的动机。即使是一个相当简单的类也必须在其界面中引入许多其他标题。

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的界面不需要Q_PRIVATE_SLOT行。

将其与非PIMPL接口进行比较,该接口将实现细节隐藏在接口的私有部分中。请注意必须包含多少其他代码。

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

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

1 个答案:

答案 0 :(得分:87)

简介

PIMPL是一个私有类,包含父类的所有特定于实现的数据。 Qt提供了一个PIMPL框架和一组在使用该框架时需要遵循的约定。 Qt的PIMPL可用于所有类,甚至是那些不是从QObject派生的类。

需要在堆上分配PIMPL。在惯用的C ++中,我们不能手动管理这样的存储,而是使用智能指针。 QScopedPointerstd::unique_ptr可以为此目的而工作。因此,最小的基于pimpl的界面(不是从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() {}

另见:

界面

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

Qt提供了几个宏和实现帮助程序,可以减少PIMPL的苦差事。实施期望我们遵循以下规则:

  • Foo的PIMPL名为FooPrivate
  • PIMPL是在接口(头文件)文件中Foo类的声明中向前声明的。

Q_DECLARE_PRIVATE宏

Q_DECLARE_PRIVATE宏必须放在班级声明的private部分。它将接口类的名称作为参数。它声明了d_func()辅助方法的两个内联实现。该方法返回具有适当const的PIMPL指针。在const方法中使用时,它返回指向 const PIMPL的指针。在非const方法中,它返回一个指向非const 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_PRIAVATE(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, method signature)

在我们的案例中:

Q_PRIVATE_SLOT(d_func(), void onAccepted())

这有效地在onAccepted类上声明了CoordinateDialog个插槽。 moc生成以下代码以调用插槽:

d_func()->onAccepted()

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

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

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:
  [...]
};

使用此宏时,必须在完全定义私有类的位置包含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内的私人类:

    // correct                  // wrong
    class FooPrivate;           class Foo {
    class Foo {                   class FooPrivate;
      ...                         ...
    };                          };
    
  • 默认情况下,类声明中的左括号后面的第一部分是私有的。因此,以下内容是等效的:

    // less wordy, preferred    // verbose
    class Foo {                 class Foo {              
      int privateMember;        private:
                                  int privateMember;
    };                          };
    
  • Q_DECLARE_PRIVATE需要接口类的名称,而不是PIMPL的名称:

    // correct                  // wrong
    class Foo {                 class Foo {
      Q_DECLARE_PRIVATE(Foo)      Q_DECLARE_PRIVATE(FooPrivate)
      ...                         ...
    };                          };
    
  • 对于不可复制/不可分配的类,例如QObject,PIMPL指针应该是const。在实现可复制类时,它可以是非const。

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

实施

必须在实现文件中定义PIMPL。如果它很大,也可以在私有标头中定义,通常名为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信号。这就是我们需要公共指针的原因:

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变量。本节稍后将对此进行描述。

实现的公共部分构造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宏,并将其传递给接口类的名称。

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

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

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

Q_Q宏

要从非const PIMPL 方法访问接口实例,我们可以使用Q_Q宏,并将其传递给接口类的名称。

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

要访问 const PIMPL 方法中的接口实例,我们在类名前添加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访问接口。如果PIMPL的方法需要操纵接口的基类或发出其信号,则通常使用它。等效的Q_DECLARE_PRIVATE宏用于允许从接口访问 PIMPL

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

宏期望指向接口的指针名为q_ptr。此宏没有两个参数变体,允许为接口指针选择不同的名称(如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的私有部分。

常见问题

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

    // 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的私有部分

    // 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指针不能是const。

回想一下在C ++ 11中,我们需要注意Rule of Four,并提供以下所有的所有:复制构造函数,移动构造函数,赋值运算符和析构函数。还有独立的swap功能来实现它,当然†。

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

接口

// Integer.h
#include <algorithm>

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();
};

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

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

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

所有这些都使用swap独立功能,我们也必须在界面中定义。请注意它是

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

实施

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

// Integer.cpp
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() {}

†符合this excellent answer:还有其他声明,我们应该为我们的类型专注std::swap,提供课堂内swap一个自由函数swap等等。但这都是不必要的:任何正确使用swap都将通过无条件的调用,我们的函数将通过ADL找到。一个功能就可以了。