Rei*_*ica 50 c++ qt pimpl-idiom
PIMPL代表P ointer到IMPL ementation.实现代表"实现细节":类的用户不必关心的东西.
Qt自己的类实现通过使用PIMPL惯用法将接口与实现完全分开.然而,Qt提供的机制没有记录.怎么用?
我想这是关于Qt中"我如何进行PIMPL"的规范性问题.答案将由下面显示的简单坐标输入对话框界面激发.
当我们有任何半复杂的实现时,使用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)
Run Code Online (Sandbox Code Playgroud)
基于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)
Run Code Online (Sandbox Code Playgroud)
就它们的公共接口而言,这两个接口完全相同.它们具有相同的信号,插槽和公共方法.
Rei*_*ica 89
PIMPL是一个私有类,包含父类的所有特定于实现的数据.Qt提供了一个PIMPL框架和一组在使用该框架时需要遵循的约定.Qt的PIMPLs可用于所有类,甚至是那些不是派生的类QObject
.
需要在堆上分配PIMPL.在惯用的C++中,我们不能手动管理这样的存储,而是使用智能指针.无论是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();
};
Run Code Online (Sandbox Code Playgroud)
析构函数的声明是必要的,因为作用域指针的析构函数需要破坏PIMPL的实例.析构函数必须在FooPrivate
类所在的实现文件中生成:
// Foo.cpp
class FooPrivate { };
Foo::Foo() : d_ptr(new FooPrivate) {}
Foo::~Foo() {}
Run Code Online (Sandbox Code Playgroud)
也可以看看:
我们现在将解释CoordinateDialog
问题中基于PIMPL的接口.
Qt提供了几个宏和实现帮助程序,可以减少PIMPL的苦差事.实施期望我们遵循以下规则:
Foo
被命名FooPrivate
.Foo
在接口(头文件)文件中的类声明中向前声明的.该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
Run Code Online (Sandbox Code Playgroud)
在我们的例子中,Q_DECLARE_PRIAVATE(CoordinateDialog)
相当于Q_DECLARE_PRIVATE_D(d_ptr, CoordinateDialog)
.
只有Qt 4兼容性或者针对非C++ 11编译器时才需要此宏.对于Qt 5,C++ 11代码,它是不必要的,因为我们可以将函子连接到信号,并且不需要显式的私有槽.
我们有时需要一个QObject
私人插槽供内部使用.这样的插槽会污染接口的私有部分.由于有关槽的信息仅与moc代码生成器相关,我们可以使用Q_PRIVATE_SLOT
宏来告诉moc通过d_func()
指针而不是通过指针调用给定的槽this
.
moc所期望的语法Q_PRIVATE_SLOT
是:
Q_PRIVATE_SLOT(instance_pointer, method signature)
Run Code Online (Sandbox Code Playgroud)
在我们的情况下:
Q_PRIVATE_SLOT(d_func(), void onAccepted())
Run Code Online (Sandbox Code Playgroud)
这有效地声明onAccepted
了CoordinateDialog
类中的一个槽.moc生成以下代码以调用插槽:
d_func()->onAccepted()
Run Code Online (Sandbox Code Playgroud)
宏本身有一个空的扩展 - 它只向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:
[...]
};
Run Code Online (Sandbox Code Playgroud)
使用此宏时,必须在完全定义私有类的位置包含moc生成的代码.在我们的情况下,这意味着该CoordinateDialog.cpp
文件应结尾有:
#include "moc_CoordinateDialog.cpp"
Run Code Online (Sandbox Code Playgroud)
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(...);
... ...
}; };
Run Code Online (Sandbox Code Playgroud)PIMPL 本身不能是私有类Foo
:
// correct // wrong
class FooPrivate; class Foo {
class Foo { class FooPrivate;
... ...
}; };
Run Code Online (Sandbox Code Playgroud)默认情况下,类声明中的左括号后面的第一部分是私有的.因此以下是等价的:
// less wordy, preferred // verbose
class Foo { class Foo {
int privateMember; private:
int privateMember;
}; };
Run Code Online (Sandbox Code Playgroud)该Q_DECLARE_PRIVATE
预期接口类的名称,而不是PIMPL的名字:
// correct // wrong
class Foo { class Foo {
Q_DECLARE_PRIVATE(Foo) Q_DECLARE_PRIVATE(FooPrivate)
... ...
}; };
Run Code Online (Sandbox Code Playgroud)对于不可复制/不可分配的类,PIMPL指针应该是const QObject
.实现可复制类时,它可以是非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*);
};
Run Code Online (Sandbox Code Playgroud)
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
}
Run Code Online (Sandbox Code Playgroud)
该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);
}
Run Code Online (Sandbox Code Playgroud)
该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() {}
Run Code Online (Sandbox Code Playgroud)
该Q_D
宏声明一个局部CoordinateDialogPrivate * const d
变量.它在下面描述.
要在接口方法中访问PIMPL ,我们可以使用Q_D
宏,将接口类的名称传递给它.
void Class::foo() /* non-const */ {
Q_D(Class); /* needs a semicolon! */
// expands to
ClassPrivate * const d = d_func();
...
Run Code Online (Sandbox Code Playgroud)
要在const接口方法中访问PIMPL ,我们需要在类名前加上const
关键字:
void Class::bar() const {
Q_D(const Class);
// expands to
const ClassPrivate * const d = d_func();
...
Run Code Online (Sandbox Code Playgroud)
要从非const PIMPL方法访问接口实例,我们可以使用Q_Q
宏,将接口类的名称传递给它.
void ClassPrivate::foo() /* non-const*/ {
Q_Q(Class); /* needs a semicolon! */
// expands to
Class * const q = q_func();
...
Run Code Online (Sandbox Code Playgroud)
要在const PIMPL方法中访问接口实例,我们在类名前加上const
关键字,就像我们为Q_D
宏做的那样:
void ClassPrivate::foo() const {
Q_Q(const Class); /* needs a semicolon! */
// expands to
const Class * const q = q_func();
...
Run Code Online (Sandbox Code Playgroud)
该宏是可选的,用于允许从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;
...
};
Run Code Online (Sandbox Code Playgroud)
该宏删除复制构造函数和赋值运算符.它必须出现在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.
Run Code Online (Sandbox Code Playgroud)该Q_DISABLE_COPY
宏必须出现在PIMPL的私人部分
// correct // wrong
// Foo.cpp // Foo.cpp
class FooPrivate { class FooPrivate {
Q_DISABLE_COPY(FooPrivate) public:
... Q_DISABLE_COPY(FooPrivate)
}; ...
};
Run Code Online (Sandbox Code Playgroud)PIMPL惯用法允许人们实现可复制,可复制和可移动构造的可分配对象.分配是通过复制和交换习惯完成的,防止代码重复.当然,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();
};
Run Code Online (Sandbox Code Playgroud)
为了提高性能,应在接口(头文件)文件中定义移动构造函数和赋值运算符.他们不需要直接访问PIMPL:
Integer::Integer(Integer && other) : Integer() {
swap(*this, other);
}
Integer & Integer::operator=(Integer other) {
swap(*this, other);
return *this;
}
Run Code Online (Sandbox Code Playgroud)
所有这些都使用swap
独立功能,我们必须在界面中定义.请注意它是
void swap(Integer& first, Integer& second) /* nothrow */ {
using std::swap;
swap(first.d_ptr, second.d_ptr);
}
Run Code Online (Sandbox Code Playgroud)
这很简单.我们不需要从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() {}
Run Code Online (Sandbox Code Playgroud)
†根据这个优秀的答案:还有其他声称我们应该专门std::swap
为我们的类型,提供一个在课堂swap
上的自由功能swap
,等等.但这是不必要的:任何正确的使用swap
将通过一个不合格的电话,我们的功能将通过ADL找到.一个功能就可以了.
归档时间: |
|
查看次数: |
13198 次 |
最近记录: |