Q_D指针
什么是 d-pointer
如果你曾经查看过Qt的源代码文件,例如 这个 Q_D
和 Q_Q
宏定义。本文就来揭开这些宏使用的目的。
Q_D
和 Q_Q
宏定义是d-pointer(也被称为 opaque pointer)设计模式的一部分, 它可以把一个类库的实施细节对使用的用户隐藏, 而且对实施的更改不会打破二进制兼容。
什么是二进制兼容
在设计像 Qt 这样的类库的时候,理想的行为应该是动态连接到 Qt 的应用程序,甚至在 Qt 类库升级或者替换到另外一个版本的时候,不需要重新编译就可以继续运行。例如,你的应用程序 CuteApp 是基于 Qt 4.5 的,你应该可以把你的 Qt 由4.5升级到 Qt 4.6 (在 Windows 下用安装程序,在 Linux 下通常有包管理器自动完成),而你用 Qt 4.5 构建的 CuteApp 应该还可以运行。
什么会打破二进制兼容
那么,什么时候类库的变化需要应用程序的重新编译呢? 我们来看一个简单的例子:
1 | class Widget { |
这里,我们有一个 Widget, 包含一个 geometry 作为成员变量。 我们编译 Widget 并且发布为 WidgetLib 1.0。
对于 WidgetLib 1.1 , 有人有了新的主意要添加样式表支持。没问题,我们只需要添加一个新的方法和一个新的 数据成员。
1 | class Widget { |
我们发布 WidgetLib 1.1,仅仅包含上面的变化,却发现和 WidgetLib 1.0一起编译并且运行正常的 CuteApp 光荣地崩溃了!
为什么会崩溃
究其原因,通过添加了一个新的数据成员,我们最终改变了 Widget
和 Label
对象的大小。为什么会这样?因为当你的C++编译器生成代码的时候,他会用偏移量
来访问对象的数据。
下面是一个 POD 对象在内存里面布局的一个简化版本。
Label 对象在 WidgetLib 1.0的布局 |
Label 对象在 WidgetLib 1.1的布局 |
---|---|
m_geometry <偏移量 0> | m_geometry <偏移量 0> |
- | m_stylesheet <偏移量 1> |
m_text <偏移量 1> | - |
- | m_text <偏移量 2> |
在 WidgetLib 1.0中,Label 的 text 成员在(逻辑)偏移量为1的位置。在编译器生成的代码里,应用程序的方法 Label::text()
被翻译成访问 Label 对象里面偏移量为1的位置。
在 WidgetLib 1.1中,Label 的 text 成员的(逻辑)偏移量被转移到了2的位置!由于应用程序没有重新编译,它仍然认为 text
在偏移量1的位置,结果却访问了 stylesheet
变量!
我确信,这个时候,会有人问,为什么Label::text()
的偏移量的计算的代码会在CuteApp二进制文件结束,而不是在WidgetLib的二进制文件。 答案是因为Label::text()
的代码定义在头文件里面,最终被内联。
那么,如果 Label::text()
没有定义为内联函数,情况会改变吗?这么讲,Label::text()
被移到源文件里面?嗯,不会。C编译器依赖对象大小在编译时和运行时相同。比如,堆栈的 winding/unwinding - 如果你在堆栈上创建了一个 Label 对象, 编译器产生的代码会根据 Label 对象在编译时的大小在堆栈上分配空间。由于Label的大小在 WidgetLib 1.1 运行时已经不同,Label 的构造函数会覆盖已经存在的堆栈数据,最终破坏堆栈。
不要改变导出的 C++ 类的大小
总之,一旦你的类库发布了,永远不要改变 导出的 C++ 类的大小或者布局(不要移动成员)。C++ 编译器生成的代码会假定,一个类的大小和成员的顺序 编译后 就不会改变.
那么,如何在不改变对象的大小的同时添加新的功能呢?
d-pointer
诀窍是通过保存唯一的一个指针而保持一个类库所有公共类的大小不变。这个指针指向一个包含所有数据的私有的(内部的)数据结构。内部结构的大小可以增大或者减小,而不会对应用程序带来副作用,因为指针只会被类库里面的代码访问,从应用程序的视角来看,对象的大小并没有改变 - 它永远是指针的大小。 这个指针被叫做 d-pointer 。
这个模式的精髓可以由下面的代码来概述(本文中的所有代码都没有析构函数,在实际使用的时候应该加上它)。
1 | /* widget.h */ |
有了上面的机构,CuteApp 从来不需要直接访问 d-pointer。由于 d-pointer 只是在 WidgetLib 被访问,而 WidgetLib 在每一次发布都被重新编译,私有的类可以随意的改变而不会对 CuteApp 带来影响。
d-pointer 的其它好处
这里不全都是和二进制兼容有关。d-pointer 还有其它的好处:
- 隐藏了实现细节 - 我们可以只发布带有头文件和二进制文件的 WidgetLib。源文件可以是闭源代码的。
- 头文件很干净,不包含实现细节,可以直接作为 API 参考。
- 由于实施需要的包含的头文件从头文件里已到了实施(源文件)里面,编译速更快。(译:降低了编译依赖)
事实上,上边的好处是微乎其微的。Qt 使用 d-pointer 的真正原因是为了二进制兼容和 Qt 最初是封闭源代码的.(译:Qt 好像没有封闭源代码)
q-pointer
到目前为止,我们仅仅看到的是作为 C 风格的数据机构的 d-pointer。实际上,它可以包含私有的方法(辅助函数)。例如,LabelPrivate
可以有一个getLinkTargetFromPoint()
辅助函数,当鼠标点击的时候找到目标链接。在很多情况下,这些辅助函数需要访问公有类,也就是 Label 或者它的父类 Widget 的一些函数。比如,一个辅助函数 setTextAndUpdateWidget()
想要调用一个安排重画Widget的公有方法 Widget::update()
。所以,WidgetPrivate
存储了一个指向公有类的指针,称为q-pointer。修改上边的代码引入q-pointer,我们得到下面代码:
1 | /* widget.h */ |
进一步优化
对于上边的代码,创建一个 Label 会带来 LabelPrivate
和 WidgetPrivate
的内存分配。如果我们在Qt里面采用这种策略,对已一些像 QListWidget
的类,情况会相当糟糕 - 它有6层的继承层次,也就会带来最多6次的内存分配。
通过对我们的 私有 类添加一个继承层次,解决了这个问题,这样类实例化时将一个 d-pointer 层层向上传递。
1 | /* widget.h */ |
1 |
|
是不是很漂亮?现在当我们创建一个 Label
对象时,它会创建一个 LabelPrivate
(它继承了 WidgetPrivate
)。它把一个 d-pointer 实体传递给Widget的保护的构造函数。Label 也有这样一个保护的构造函数,可以被继承 Label
的类提供自己的私有类来使用。
把 q-ptr 和 d-ptr 转型到正确的类型(QPTR 和 DPTR)
上一步优化的一个副作用是 q-ptr 和 d-ptr 的类型分别是 Widget
和 WidgetPrivate
。
这就意味着下面的代码不能工作。
1 | void Label::setText(const String &text) { |
因此,在子类里访问 d-pointer 的时候,需要用 static_cast 转型到合适的类型。
1 | void Label::setText(const String &text) { |
代码里到处都是 static_cast 看起来不是那么漂亮,所以我们定义了下面的宏,
1 | // global.h (macros) |
Qt 中的 d-pointers
在 Qt 中,几乎所有的公有类都使用了 d-pointer。唯一不用的情况是如果事先知道某个类永远不会添加额外的成员变量。例如,像 QPoint
, QRect
这些类,我们不期望有新的成员添加,因此它们的数据成员直接保存在类里而没用 d-pointer。
- 在Qt中,所有私有对象的基类是
QObjectPrivate
Q_D
和Q_Q
宏提供了上边讨论的 QPTR 和 DPTR 的功能.- Qt 的公有类有一个
Q_DECLARE_PRIVATE
的宏。这个宏的代码:
1 | // qlabel.h |
这里的想法是 QLabel
提供了一个函数 d_func()
允许访问它的私有内部类。这个方法本身是私有的(因为这个宏定义在 qlabel.h 私有区域)。 但是 d_func()
可以被 QLabel
的 友元函数 (C++ 友元)调用。这主要对一些 Qt 类想获得 QLabel
的无法通过公有 API 访问的一些信息有用。例如,QLabel
可能要跟踪用户点击了一个链接多少次。但是没有公有 API 访问这个信息。QStatistics
是需要这个信息的一个类。Qt开发人员可以添加 QStatistics
作为 QLabel
的一个友元类,这样QStatistics
就可以 label->d_func()->linkClickCount
来访问。
d_func
还有一个优点是保证了const正确性:在 MyClass 的一个 const 成员函数里,你需要 Q_D(const MyClass),因此你只能调用 MyClassPrivate 的const成员函数。如果用 “naked” 的 d_ptr 你可以调用非const函数。