这一篇文章介绍QT框架中QT对象类型也就是QObject类型的源代码在设计上的一个比较优秀的设计思想。
1. QObject类型定义
QObject
直接来看QObject的源代码。为了表达更简洁更直观,这里省略了跟本文无关的各种代码。
如果查看QObject类型定义,可以发现类型里面有很多成员函数,但是除了 d_ptr之外,就没有定义更多的成员变量。当然,没有定义更多成员变量并不等于QObject对象实例中就只有d_ptr这一个数据,实际上由于QObject中定义了虚函数,因此QObject对象实例中还有vptr,也就是指向虚函数表的指针。
现在让我们来讨论这么一个问题,QObject肯定是有内部状态数据的,那么内部状态数据保存在哪儿呢?实际上就是保存在d_ptr指向的QT对象数据对象实例中。d_ptr是QT中的一个范围指针,也就是说当QObject对象被销毁时,使得d_ptr指向的QT对象数据对象实例也会被销毁掉。
QObjectData
先来看QObjectData类型定义。
这个类型里面定义了一个QObject指针类型的变量q_ptr。在QT对象数据类型有关的各种成员函数中,如果想访问所属于的QObject对象的this指针或者成员函数,可以通过q_ptr这个指针来访问。
也就是说QObject和QObjectData之间相互持有了对方的对象实例的指针,实现了QT对象和QT对象数据的之间的双向访问。
QObjectPrivate
这个类型是QObjectData类型的派生类型。定义了QT对象的一些私有数据。
2. QT对象的实际对象数据到底是什么
QObject对象
先来看一下QObject类型对象实例中的对象数据是什么。
如果跟踪调试一下,可以看到QObject类型构造函数的源代码。
可以看到在构造函数中一开始就实现了QObject和QObjectPrivate对象实例的双向引用,当然严格来讲是双向的指针指向。
现在新的问题来了,QT框架中有很多具体的QT对象类型,虽然它们都是QObject的直接或间接派生类型,但是各自必然都具有各自类型的独特的私有数据。那么QT框架是如何实现这些类型的对象实例在构造时使用自己类型独特的私有数据的呢?
QThread是一个典型正面例证
直接看QThread类型定义:
然后看一下QThread构造函数:
QThread在构造对象实例时先构造一个QThreadPrivate作为自己的私有数据对象实例,然后让d_ptr指针指向这个私有数据对象实例。
实际上不同的QObject派生类型的对象实例中,都会让d_ptr指向自己独特的私有数据对象实例。也就是QObject的d_ptr和QObjectData的q_ptr这两个指针指向的对象实例之间存在双向相互持有对方指针的情况。
QWidget是一个反面特例
QT框架中除了像QThread这样严格遵守一个QT对象实例只包含一个d_ptr范围指针的情况之外,还有一些特例,比如QWidget就是一个不严格遵守这种情况的特例。
QWidget的类型定义:
看一下QWidget的构造函数:
尽管QWidget除了QObject的d_ptr指针之外还有一个自己的指针data,但是看起来似乎仍然是符合这种不定义具体数据成员之定义指针的这种设计思想的。实际上并非如此。来看一下QPaintDevice类型的定义。
显然QPaintDevice这个类型中除了一个指针之外还有非指针类型的成员变量painters。尽管QPaintDevice并非QObject派生类型,但是肯定是影响到了QWidget的内存布局。
3. 问题根源和解决方案
问题根源
QT框架会使用d_ptr这么一个范围指针,当然QT框架中还有很多类型使用的是原始指针类型。使用这么一个指针类型显然会带来编码上的一些额外的工作量,比如不能直接访问成员变量,而只能通过指针间接访问。
既然使用指针吃力不讨好,为什么QT框架要使用这么一个指针呢?无利不起早,QT框架的设计者不可能吃饱了撑的没事找事。d_ptr必然是解决了一些实际问题才有存在的价值。
先来看问题是怎么产生的。
假定有一个类型定义在butianyunobject.h文件中。
这个类型直接把私有数据成员变量定义在ButianyunObject类型本身,这在一些应用场景下会带来一些问题。
首先是编译问题。
如果有10个cpp文件#include了butianyunobject.h文件,那么一旦对这个.h文件的数据成员变量做那么一丁点修改,在下一次编译这个项目时就会导致这个10个cpp文件全部都必须重新编译。这也就是直接将类型的数据成员变量定义在类型本身的缺点。
其次是依赖问题。
如果说带来的不必要的重新编译问题只是一个项目组内部的技术问题,影响范围有限,那么现在讨论的依赖问题可能影响范围会比较大一点。
考虑这样一个场景:这个代码出现在一个对外公开发布的动态链接库中,而且作为一个公开导出接口,很多客户项目产品中使用了这个公开导出接口。
现在库开发项目组中如果有人为了修复BUG,在某个新版本中修改了一下这个ButianyunObject类型的私有数据成员变量然后将这个动态链接库和对应的头文件公开发布出去,会产生什么问题呢?或者说会影响到客户项目产品码?
答案是一定会影响到使用新版本的客户项目产品。一个类型的数据成员变量修改之后,会导致类型的对象实例的内存布局发生变化,这意味着所有想使用新版本的客户项目产品的软件必须使用新的头文件重新编译,而无法直接使用新版本的动态链接库去替换旧版本的动态链接库。
解决方案
然后来看解决方案,或者说设计思路。
如果将ButianyunObject类型这么来定义就可以避免这些问题。
也就是将类型的私有数据成员变量全部抽取到一个私有数据类型中,然后将这个私有数据类型定义在.cpp文件中,而不是.h文件中。在对外公开的数据类型中只定义了一个指针类型的变量d_ptr, d_ptr指向实际的私有数据对象实例。
这样就算是修改私有数据成员变量,也不会导致对外公开接口发生任何变化。也就不会引起内部编译问题和外部依赖问题。
4. 间接的设计思想
这种设计思想就是所说的C++ PIMP。PIMP=Pointer to Implement,也就是指向具体实现的指针,说白了就是使用指向对象的具体私有数据对象实例的指针代替直接定义私有数据本身。
QT框架中实际上大量使用了PIMP设计思想。
下面再进一步进行抽象思考,再拔高一个层次,所谓PIMP,就是间接的思想的具体体现或者具体应用。实际上C/C++的指针类型的“指向”的含义本来自带一层“间接”的意思。
间接的思想是所有设计模式的最本质最根本的底层设计思想和底层思考逻辑。可以这么讲,没有哪一个设计模式不是对间接的思想的具体体现。
当然再拔高了讲,大部分软件架构也都是在某一个层面上充分应用了间接的思想。一般的思考逻辑也是将现状抽象一下,然后在某个点上使用一个软件框架来实现原来直接用几行代码去做的事情,也就是间接的使用一个更复杂的抽象逻辑框架去解决原来直接去做带来的问题。
当然间接的思想带来的另外一个天然的好处就是自然而然的实现了关注点分离,对于公开接口的使用者而言,根本看不到一个框架或者类的内部实现细节。