如何使用Qt的PIMPL成语?
PIMPL代表执行进程 。 实现代表“实现细节”:类的用户不需要关心的东西。
Qt自己的类实现通过使用PIMPL惯用语将接口与实现完全分开。 然而,由Qt提供的机制没有logging。 如何使用它们?
我想这是关于“我如何PIMPL”在Qt的规范问题。 答案将由下面显示的一个简单的坐标input对话框界面来激励。
当我们有一个半复杂的实现时,使用PIMPL的动机变得明显。 进一步的动机在这个问题上给出。 即使是一个相当简单的类也必须在其接口中引入大量其他头文件。

基于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) 
这两个接口就其公共接口而言是完全等价的 。 他们有相同的信号,插槽和公共方法。
介绍
  PIMPL是一个私有类,它包含父类的所有特定于实现的数据。  Qt提供了一个PIMPL框架和一套使用该框架时需要遵循的约定。  Qt的PIMPL可以用在所有类中,即使那些不是从QObject派生的。 
  PIMPL需要在堆上分配。 在惯用的C ++中,我们不能手动pipe理这种存储,而是使用一个智能指针。  QScopedPointer或std::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 。
界面
 现在我们将在这个问题中解释基于PIMPL的CoordinateDialog接口。 
Qt提供了几个macros和实现帮助器,可以减lessPIMPL的苦差事。 实施期望我们遵循这些规则:
-   Foo类的PIMPL被命名为FooPrivate。
-   PIMPL在接口(头文件)中沿着Foo类的声明进行前向声明。
Q_DECLARE_PRIVATEmacros
  Q_DECLARE_PRIVATEmacros必须放在类的声明的private部分。 它将接口类的名称作为参数。 它声明了d_func()辅助方法的两个内联实现。 该方法返回具有适当的常量的PIMPL指针。 当在const方法中使用时,它返回一个指向const PIMPL的指针。 在非const方法中,它返回一个指向非const的PIMPL的指针。 它还提供了派生类中正确types的pimpl。 因此,实现中所有对d_func()访问都是使用d_func()和**而不是通过d_ptr来d_ptr 。 通常我们使用Q_Dmacros,如下面的实现部分所述。 
macros有两种口味:
 Q_DECLARE_PRIVATE(Class) // assumes that the PIMPL pointer is named d_ptr Q_DECLARE_PRIVATE(Dptr, Class) // takes the PIMPL pointer name explicitly 
 在我们的例子中, Q_DECLARE_PRIAVATE(CoordinateDialog)相当于Q_DECLARE_PRIVATE(d_ptr, CoordinateDialog) 。 
Q_PRIVATE_SLOTmacros
这个macros只适用于Qt 4兼容性,或者针对非C ++ 11编译器。 对于Qt 5,C ++ 11代码,这是不必要的,因为我们可以将函子连接到信号,并且不需要明确的专用插槽。
 我们有时需要一个QObject有专用插槽供内部使用。 这样的插槽会污染接口的专用部分。 由于关于插槽的信息仅与moc代码生成器相关,我们可以使用Q_PRIVATE_SLOTmacros来告诉moc通过d_func()指针调用给定的插槽,而不是通过this 。 
 在Q_PRIVATE_SLOT ,moc预期的语法是: 
 Q_PRIVATE_SLOT(instance_pointer, method signature) 
在我们的情况下:
 Q_PRIVATE_SLOT(d_func(), void onAccepted()) 
 这有效地声明了CoordinateDialog类中的onAccepted插槽。  moc生成以下代码来调用该插槽: 
 d_func()->onAccepted() 
macros本身有一个空的扩展 – 它只向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: [...] }; 
 当使用这个macros时,你必须在私有类完全定义的地方包含moc生成的代码。 在我们的例子中,这意味着CoordinateDialog.cpp文件应该以: 
 #include "moc_CoordinateDialog.cpp" 
陷阱
- 
所有要在类声明中使用的 Q_macros都已经包含分号。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) ... ... }; };
- 
PIMPL指针应该是不可复制/不可分配的类(如 QObject常量。 实现可复制类时,它可以是非const的。
- 
由于PIMPL是一个内部实现细节,因此在使用该接口的站点上它的大小是不可用的。 使用放置新的和Fast Pimpl成语的诱惑应该被抵制,因为它对除了一个根本不分配内存的类没有任何好处。 
实施
  PIMPL必须在实现文件中定义。 如果它很大,也可以在一个私有头文件中定义,通常用foo_p.h来表示一个接口在foo.h 。 
PIMPL至less只是主类数据的载体。 它只需要一个构造函数,而不需要其他方法。 在我们的例子中,它也需要存储指向主类的指针,因为我们要从主类发出一个信号。 从而:
 // 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来显式禁用复制function。 
  Q_DECLARE_PUBLICmacros与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 / non-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_Qmacros声明一个本地的CoordinateDialog * const qvariables。 这部分将在后面介绍。 
实现的公共部分构造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_Dmacros声明一个本地的CoordinateDialogPrivate * const dvariables。 它在下面描述。 
Q_Dmacros
 要在接口方法中访问PIMPL,我们可以使用Q_Dmacros,并将它传递给接口类的名称。 
 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_Qmacros
 要从非const的PIMPL方法访问接口实例,我们可以使用Q_Qmacros,并将其传递给接口类的名称。 
 void ClassPrivate::foo() /* non-const*/ { Q_Q(Class); /* needs a semicolon! */ // expands to Class * const q = q_func(); ... 
 为了在const PIMPL方法中访问接口实例,我们用const关键字预先给出了类名,就像我们为Q_Dmacros所做的Q_D : 
 void ClassPrivate::foo() const { Q_Q(const Class); /* needs a semicolon! */ // expands to const Class * const q = q_func(); ... 
Q_DECLARE_PUBLICmacros
 该macros是可选的,用于允许从PIMPL访问接口 。 如果PIMPL的方法需要操纵接口的基类,或者发出信号,通常使用它。 使用等价的Q_DECLARE_PRIVATEmacros允许从接口访问PIMPL 。 
 macros将接口类的名称作为参数。 它声明了q_func()辅助方法的两个内联实现。 该方法返回具有适当的常量的接口指针。 当在const方法中使用时,它返回一个指向const接口的指针。 在非const方法中,它返回一个指向非const接口的指针。 它还提供了派生类中正确types的接口。 因此,从PIMPL内部对接口的所有访问都是使用q_func()和**而不是通过q_ptr来q_ptr 。 通常我们会使用上面描述的Q_Qmacros。 
 macros期望指向接口的指针被命名为q_ptr 。 这个macros没有两个参数的变体,可以为接口指针select一个不同的名称(就像Q_DECLARE_PRIVATE )。 
macros观扩展如下:
 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_COPYmacros
该macros将删除复制构造函数和赋值运算符。 它必须出现在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_COPYmacros必须出现在PIMPL的私有部分// correct // wrong // Foo.cpp // Foo.cpp class FooPrivate { class FooPrivate { Q_DISABLE_COPY(FooPrivate) public: ... Q_DISABLE_COPY(FooPrivate) }; ... };
PIMPL和非QObject可复制类
PIMPL习惯用法允许实现可复制,复制和移动可构build的可分配对象。 这个任务是通过copy-and-swap成语完成的,防止代码重复。 当然,PIMPL指针不能是const。
 回想一下在C ++ 11中,我们需要注意四条规则 ,并提供以下所有内容:复制构造函数,移动构造函数,赋值运算符和析构函数。 而独立的swapfunction当然是全部实现的。 
我们将用一个相当无用但却是正确的例子来说明这一点。
接口
 // 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独立function,我们必须在界面中定义。 请注意,这是 
 void swap(Integer& first, Integer& second) /* nothrow */ { using std::swap; swap(first.d_ptr, second.d_ptr); } 
履行
 这很简单。 我们不需要从PIMPL访问接口,因此Q_DECLARE_PUBLIC和q_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() {} 
  †根据这个优秀的答案 :还有其他的声明,我们应该专门为我们的typesstd::swap ,提供一个在自由functionswap ,同类swap等,但这是不必要的:任何正确使用swap将通过一个不合格的电话,我们的function将通过ADL发现。 一个function就可以了。