Qt学习-D-Pointer
原文地址 https://wiki.qt.io/D-Pointer
前言
d-point是Qt的一种代码设计模式,可以使库的实现细节可能对其用户隐藏,并且可以在不破坏二进制兼容性(Binary compatibility)的情况下对库进行实现更改
二进制兼容
sdk库升级后,动态链接该库的某个app不必重新编译,仍然可以运行,可以认为该库是二进制兼容的
某些情况下会破坏二进制兼容性,例如
class Widget
{
// ...
private:
Rect m_geometry;
};
class Label : public Widget
{
public:
// ...
String text() const
{
return m_text;
}
private:
String m_text;
};
以上代码发布为WidgetLib 1.0.
而在WidgetLib 1.1版本中,在Widget类中增加了成员变量m_stylesheet
class Widget
{
// ...
private:
Rect m_geometry;
String m_stylesheet; // NEW in WidgetLib 1.1
};
class Label : public Widget
{
public:
// ...
String text() const
{
return m_text;
}
private:
String m_text;
};
升级为WidgetLib 1.1后,原来使用WidgetLib 1.0的app就会crash
原因是通过添加一个新的数据成员,我们最终改变了 Widget 和 Label 对象的大小。当C++ 编译器生成代码时,它使用偏移量来访问对象内的数据。
Label object layout in WidgetLib 1.0 | Label object layout in WidgetLib 1.1 |
---|---|
m_geometry <offset 0> | m_geometry <offset 0> |
- - - | m_stylesheet <offset 1> |
m_text <offset 1> | - - - |
- - - | m_text <offset 2> |
Label::text()原来访问的offset 1的位置m_text,现在是m_stylesheet的位置,就会crash
至于为什么 Label::text() 的偏移量计算代码最终出现在 CuteApp 二进制文件中而不是 WidgetLib 二进制文件中。 答案是 Label::text() 的代码是在头文件中定义的,编译器最终将其内联。
如果Label::text()不是内联的,也会有问题。C++ 编译器依赖于对象的大小在编译时和运行时相同。 例如,堆栈卷绕/展开——如果您在堆栈上创建了一个 Label 对象,编译器会生成代码以在编译时根据 Label 的大小在堆栈上分配空间。 由于 Label 的大小在 WidgetLib 1.1 的运行时是不同的,Label 的构造函数会覆盖现有的堆栈数据并最终破坏堆栈。
总之,一旦您的库发布,永远不要更改导出的(即对用户可见的)C++ 类的大小或布局(不要四处移动数据)。 C++ 编译器生成代码时假定类中数据的大小或顺序在编译应用程序后不会改变。
d-point
诀窍是通过仅存储单个指针来保持库的所有公共类的大小不变。 该指针指向包含所有数据的私有/内部数据结构。 这个内部结构的大小可以收缩或增长而不会对应用程序产生任何副作用,因为指针仅在库代码中访问,并且从应用程序的角度来看,对象的大小永远不会改变 - 它始终是指针。 该指针称为 d 指针(d-point)。
以下是其实现代码
widget.h
class WidgetPrivate;
class Widget
{
// ...
Rect geometry() const;
// ...
private:
WidgetPrivate *d_ptr;
};
这里WidgetPrivate作为内部类,不包含其头文件,仅做前置声明
widget_p.h,WidgetPrivate类的头文件
struct WidgetPrivate
{
Rect geometry;
String stylesheet;
};
widget.cpp
#include "widget_p.h"
Widget::Widget() : d_ptr(new WidgetPrivate)
{
// Creation of private data
}
Rect Widget::geometry() const
{
// The d-ptr is only accessed in the library code
return d_ptr->geometry;
}
在这里#include “widget_p.h”
Label类继承Widget类,Label类也有自己的私有类LabelPrivate
class Label : public Widget
{
// ...
String text();
private:
// Each class maintains its own d-pointer
LabelPrivate *d_ptr;
};
label.cpp
// Unlike WidgetPrivate, the author decided LabelPrivate
// to be defined in the source file itself
struct LabelPrivate
{
String text;
};
Label::Label() : d_ptr(new LabelPrivate)
{
}
String Label::text()
{
return d_ptr->text;
}
通过上述结构,app 永远不会直接访问 d 指针。由于 d 指针仅在 WidgetLib 中访问,并且 WidgetLib 会在每次发布时重新编译,因此 Private 类可以自由更改,而不会影响 app。
使用d-point还有其他的好处
- 隐藏了实现细节——我们可以仅将 WidgetLib 与头文件和二进制文件一起发布。 .cpp 文件可以是封闭源代码。
- 头文件没有实现细节,可以作为 API 参考。
- 由于实现所需的头文件已从头文件移动到实现(源)文件中,因此编译速度要快得多。
q-point
到目前为止,我们只看到了 d 指针作为 C 风格的数据结构。 实际上,它包含私有方法(辅助函数)。 例如,LabelPrivate 可能有一个 getLinkTargetFromPoint() 辅助函数,单击鼠标时需要它来查找链接目标。 在许多情况下,这些辅助方法需要访问公共类,即一些来自 Label 或其基类 Widget 的函数。 例如,辅助方法 setTextAndUpdateWidget() 可能需要调用 Widget::update(),这是一个公共方法来安排重绘 Widget。 因此,WidgetPrivate 存储了一个指向公共类的指针,称为 q 指针(q-point)。 为 q 指针修改上面的代码,我们得到:
widget.h
class WidgetPrivate;
class Widget
{
// ...
Rect geometry() const;
// ...
private:
WidgetPrivate *d_ptr;
};
widget_p.h
struct WidgetPrivate
{
// Constructor that initializes the q-ptr
WidgetPrivate(Widget *q) : q_ptr(q) { }
Widget *q_ptr; // q-ptr points to the API class
Rect geometry;
String stylesheet;
};
widget.cpp
#include "widget_p.h"
// Create private data.
// Pass the 'this' pointer to initialize the q-ptr
Widget::Widget() : d_ptr(new WidgetPrivate(this))
{
}
Rect Widget::geometry() const
{
// the d-ptr is only accessed in the library code
return d_ptr->geometry;
}
label.h
class Label : public Widget
{
// ...
String text() const;
private:
LabelPrivate *d_ptr;
};
label.cpp
struct LabelPrivate
{
LabelPrivate(Label *q) : q_ptr(q) { }
Label *q_ptr;
String text;
};
Label::Label() : d_ptr(new LabelPrivate(this))
{
}
String Label::text()
{
return d_ptr->text;
}
在上面的代码中,创建单个 Label 导致为 LabelPrivate 和 WidgetPrivate 分配内存。 如下图所示
如果我们对 Qt 采用这种策略,对于像 QListWidget 这样的类,情况会变得更糟——它在类继承层次结构中有 6 层深,这将导致多达 6 次内存分配!
可以通过私有类设置继承层次结构并让实例化的类一直向上传递 d 指针来优化
请注意,在继承 d 指针时,私有类的声明必须在单独的文件中,例如 widget_p.h。不再可能在 widget.cpp 文件中声明它。
widget.h
class Widget
{
public:
Widget();
// ...
protected:
// only subclasses may access the below
// allow subclasses to initialize with their own concrete Private
Widget(WidgetPrivate &d);
WidgetPrivate *d_ptr;
};
widget_p.h
struct WidgetPrivate
{
WidgetPrivate(Widget *q) : q_ptr(q) { } // constructor that initializes the q-ptr
Widget *q_ptr; // q-ptr that points to the API class
Rect geometry;
String stylesheet;
};
widget.cpp
Widget::Widget() : d_ptr(new WidgetPrivate(this))
{
}
Widget::Widget(WidgetPrivate &d) : d_ptr(&d)
{
}
label.h
class Label : public Widget
{
public:
Label();
// ...
protected:
Label(LabelPrivate &d); // allow Label subclasses to pass on their Private
// notice how Label does not have a d_ptr! It just uses Widget's d_ptr.
};
label.cpp
#include "widget_p.h"
class LabelPrivate : public WidgetPrivate
{
public:
String text;
};
Label::Label()
: Widget(*new LabelPrivate) // initialize the d-pointer with our own Private
{
}
Label::Label(LabelPrivate &d) : Widget(d)
{
}
当我们现在创建一个 Label 对象时,它会创建一个 LabelPrivate(它是 WidgetPrivate 的子类)。 它将具体的 d 指针传递给 Widget 的受保护构造函数! 现在,当创建一个 Label 对象时,只有一次内存分配。 Label 还有一个受保护的构造函数,它的子类可以使用它来提供自己的私有类。如下图所示
Qt中的d指针
在 Qt 中,几乎每个公共类都使用 d 指针方法。 唯一不使用它的情况是事先知道该类永远不会添加额外的成员变量。 例如,对于像 QPoint、QRect 这样的类,不需要添加新成员,因此数据成员直接存储到类本身而不是使用 d 指针。
请注意,在 Qt 中,所有 Private 对象的基类都是 QObjectPrivate。
Q_D and Q_Q
我们在上一步中进行的优化的一个副作用是 q-ptr 和 d-ptr 是 Widget 和 WidgetPrivate 类型。这意味着以下将不起作用。
void Label::setText(const String &text)
{
// won't work! since d_ptr is of type WidgetPrivate even though
// it points to LabelPrivate object
d_ptr->text = text;
}
因此,当访问子类中的 d 指针时,我们需要将 static_cast 转换为适当的类型。
void Label::setText(const String &text)
{
LabelPrivate *d = static_cast<LabelPrivate*>(d_ptr); // cast to our private type
d->text = text;
}
如您所见,到处都是 static_cast 并不是一件好事。相反,在 src/corelib/global/qglobal.h 中定义了两个宏,这使得它变得直截了当
global.h
#define Q_D(Class) Class##Private * const d = d_func()
#define Q_Q(Class) Class * const q = q_func()
label.cpp
// With Q_D you can use the members of LabelPrivate from Label
void Label::setText(const String &text)
{
Q_D(Label);
d->text = text;
}
// With Q_Q you can use the members of Label from LabelPrivate
void LabelPrivate::someHelperFunction()
{
Q_Q(Label);
q->selectAll();
}
Q_DECLARE_PRIVATE and Q_DECLARE_PUBLIC
Qt 类在公共类中有一个 Q_DECLARE_PRIVATE 宏。宏是这么写的:
qglobal.h
#define Q_DECLARE_PRIVATE(Class)\
inline Class##Private* d_func() {\
return reinterpret_cast<Class##Private *>(qGetPtrHelper(d_ptr));\
}\
inline const Class##Private* d_func() const {\
return reinterpret_cast<const Class##Private *>(qGetPtrHelper(d_ptr));\
}\
friend class Class##Private;
#define Q_DECLARE_PUBLIC(Class) \
inline Class* q_func() { return static_cast<Class *>(q_ptr); } \
inline const Class* q_func() const { return static_cast<const Class *>(q_ptr); } \
friend class Class;
这个宏可以这样使用:
qlabel.h
class QLabel
{
private:
Q_DECLARE_PRIVATE(QLabel)
};
这个想法是 QLabel 提供了一个允许访问其私有内部类的函数 d_func() 。 该方法本身是私有的(因为宏位于 qlabel.h 的私有部分中)。 然而,d_func() 可以被 QLabel 的友类调用。 这主要用于无法使用公共 api 访问某些 QLabel 信息的 Qt 类访问信息。 举一个奇怪的例子,QLabel 可能会跟踪用户点击链接的次数。 但是,没有公共 API 来访问此信息。 QStatistics 是一个需要此信息的类。 Qt 开发人员会将 QStatistics 添加为 QLabel 的好友,然后 QStatistics 可以执行 label->d_func()->linkClickCount。
d_func 还具有强制 const 正确性的优点:在 MyClass 的 const 成员函数中,您需要一个 Q_D(const MyClass),因此您只能调用 MyClassPrivate 中的 const 成员函数。 使用裸 d_ptr,您还可以调用非常量函数。
使用示例
mywidget.h
#ifndef MYWIDGET_H
#define MYWIDGET_H
#include <QObject>
class MyWidgetPrivate;
class MyWidget : public QObject
{
Q_OBJECT
public:
explicit MyWidget(QObject *parent = nullptr);
void fun();
void api();
private:
Q_DECLARE_PRIVATE(MyWidget)
MyWidgetPrivate *d_ptr;
};
#endif // MYWIDGET_H
mywidget_p.h
#ifndef MYWIDGETPRIVATE_H
#define MYWIDGETPRIVATE_H
#include <QObject>
#include "mywidget.h"
class MyWidgetPrivate : public QObject
{
Q_OBJECT
Q_DECLARE_PUBLIC(MyWidget)
public:
explicit MyWidgetPrivate(QObject *parent = nullptr);
void func();
private:
MyWidget *q_ptr;
QString text;
};
#endif // MYWIDGETPRIVATE_H
mywidget.cpp
#include "mywidget.h"
#include "mywidget_p.h"
#include <QDebug>
MyWidget::MyWidget(QObject *parent)
: QObject{parent}, d_ptr(new MyWidgetPrivate(this))
{
}
void MyWidget::fun()
{
Q_D(MyWidget);
return d->func();
}
void MyWidget::api()
{
qDebug() << __PRETTY_FUNCTION__;
}
mywidget_p.cpp
#include "mywidget_p.h"
#include <QDebug>
MyWidgetPrivate::MyWidgetPrivate(QObject *parent)
: QObject{parent}
{
text = "aaa";
}
void MyWidgetPrivate::func()
{
qDebug() << __PRETTY_FUNCTION__;
Q_Q(MyWidget);
q->api();
qDebug() << text;
}
main.cpp
#include "mywidget.h"
int main(int argc, char *argv[])
{
MyWidget *myWidget = new MyWidget;
myWidget->fun();
return 0;
}