PIMPL 代表 P ointer 到 IMPL mentation。实现代表“实现细节”:类的用户不需要关心的东西。
Qt 自己的类实现通过使用 PIMPL 惯用法将接口(interface)与实现清晰地分开。然而,Qt 提供的机制没有记录。如何使用它们?
我希望这是 Qt 中关于“我如何 PIMPL”的规范问题。答案是由一个简单的坐标输入对话框界面激发的,如下所示。
当我们有一个半复杂的实现时,使用 PIMPL 的动机就变得很明显了。进一步的动机在 this question 中给出。即使是一个相当简单的类也必须在其接口(interface)中引入许多其他头文件。
基于 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++ 中,我们不能手动管理这样的存储,而是使用智能指针。 QScopedPointer
或 std::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() {}
也可以看看:界面
我们现在将解释问题中基于 PIMPL 的
CoordinateDialog
接口(interface)。Qt 提供了几个宏和实现助手来减少 PIMPL 的苦差事。实现要求我们遵循以下规则:
Foo
的 PIMPL 被命名为 FooPrivate
。 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(...);
... ...
}; };
Foo
本身内的私有(private)类: // 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
需要接口(interface)类的名称,而不是 PIMPL 的名称: // correct // wrong
class Foo { class Foo {
Q_DECLARE_PRIVATE(Foo) Q_DECLARE_PRIVATE(FooPrivate)
... ...
}; };
QObject
,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)部分。
常见问题
// 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_PUBLIC
和 q_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/