PIMPL表示对于P ointer到IMPL ementation。 落实代表“实施细节”:东西之类的用户不必去关注。
Qt的自己的类实现清洁通过使用PIMPL方法的分离出来,从实现的接口。 然而,Qt提供的机制是无证。 如何使用他们?
我想这是关于“我怎么PIMPL” Qt中的规范问题。 答案是由下述简单的坐标输入对话框界面的动机。
对于使用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)
这两个接口是完全等价的 ,只要他们的公共接口关注。 它们具有相同的信号,插槽和公共方法。
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:
- A deeper exposition of the idiom.
- Gotchas and pitfalls of PIMPL.
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_PRIAVATE(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
所有的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) ... ... }; };
所述PIMPL指针应该是不可复制/非可分配的类,如常量QObject
。 实现可复制类时,它可以是非常量。
由于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宏
要访问PIMPL在接口中的方法,我们可以使用Q_D
宏,传递给它的接口类的名称。
void Class::foo() /* non-const */ {
Q_D(Class); /* needs a semicolon! */
// expands to
ClassPrivate * const d = d_func();
...
要访问PIMPL在一个const接口方法,我们需要与预先设置的类名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接口。 它还提供正确类型的派生类的接口。 由此可见,从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方法允许一个实现可复制,禁止复制和MOVE-构造,可赋值的对象。 分配是通过做复制和交换成语,防止代码重复。 该PIMPL指针不能是const的,当然。
回想一下在C ++ 11中,我们需要注意的四个规则 ,并提供以下所有内容:拷贝构造函数,构造运动,赋值操作符和析构函数。 而独立的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_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() {}
†每本优秀的答案 :有,我们应该专注其他索赔std::swap
我们的类型,提供一流swap
沿一侧具有自由功能swap
,等等。但是这一切不必要的:任何正确使用的swap
会通过不合格的呼叫,我们的功能将通过找到ADL 。 一个功能就可以了。