基于Qt 5.15源码来聊聊隐式共享

一、导读
在实际开发中,qt中很多类可以直接作为函数参数传递,这是为什么?其背后的实现机制又是什么?这些都归功于隐式共享,本文基于qt 5.15源码,来聊聊隐式共享!
二、隐式共享简介
qt中的许多c++类使用隐式数据共享来提高资源使用并减少数据复制。当这些类作为参数传递时,因为只传递一个指向数据的指针,并且只有当函数写入数据时数据才会被复制,即copy -on-write,隐式共享类是安全、高效的。
共享类由一个指向包含引用计数和数据的共享数据块的指针组成。
当创建共享对象时,它将引用计数设置为1。每当有新对象引用共享数据时,引用计数就递增,当对象解引用共享数据时,引用计数就递减,当引用计数变为零时,将删除共享数据。
在处理共享对象时,有两种方法复制对象。也就是经常谈到的:深度拷贝和浅拷贝。深度拷贝意味着复制一个对象,浅拷贝是一个引用拷贝,也就是一个指向共享数据块的指针。站在内存和cpu角度,执行一个深度拷贝可能是昂贵的操作,执行浅拷贝则非常快,因为浅拷贝只涉及设置指针和增加引用计数。
注意:隐式共享对象的对象赋值(operator=())是使用浅拷贝实现的。
隐式共享的优点是:
(1)程序不需要进行不必要的数据复制操作,从而减少内存的使用和多次执行数据复制操作。
(2)可以很容易地被赋值。
(3)可以作为函数参数传递,并从函数中返回。
三、源码角度分析隐式共享
隐式共享会自动将对象从共享块中分离出来,如果对象即将改变并且引用计数大于1,(这通常被称为写时复制或值语义。)
隐式共享类可以控制其内部数据,在任何修改其数据的成员函数中,它都会在修改数据之前自动分离。(但是,需注意容器迭代器的特殊情况,后文将说明这一点!)
此处以qpen这个隐式共享类为例,从源码角度分析qpen类是如何从更改内部数据的成员函数中分离共享数据的。在qt5.15源码中用于描述qpen的文件为qpen_p.h、qpen.cpp、qpen.h三个文件,位于源码路径(/qtbase/src/gui/painting目录)下。在qpen类定义中有一个detach():
实现如下:
detach()用于从共享pen数据中分离,以确保该pen只有一个引用数据,如果多个pen共享公共数据,这支pen将取消对数据的引用并获得数据的副本;如果只有一个则返回,什么也不做。上述代码中,qpendata实则是qpenprivate的类型别名,用于描述qpen的数据,定义如下(位于qpen_p.h文件中):
上述代码分析了detach()函数,下文以qpen的一个成员函数setstyle(qt::penstyle style)来描述,该函数实现如下:
从上述图片所示,在setstyle()函数中,会使用detach()从公共数据中分离,然后在设置style成员。
综上,如果qt提供的类支持隐式共享,那么其源码内部实现都有对应的数据管理机制,实现写时复制。
四、隐式共享在开发中的使用
上述第二节描述了隐式共享的qpen类如何从更改内部数据的成员函数中分离共享数据。可简化为下述代码片段:
void qpen::setstyle(qt::penstyle s){    detach();           // 从公共数据中分离    d->style = s;    // 设置style成员}void qpen::detach(){    if (d->ref != 1) {        ...             // 执行深度拷贝    }}  
所以,在开发中如果更改了对象,类将自动与公共数据分离,甚至不会注意到这些对象是共享的。因此,可以将它们的单独实例视为单独的对象,它们始终作为独立的对象。但在有些情况下可以共享数据,因此可以将这些类的实例作为参数按值传递给函数,而不必考虑复制开销。
例如下列代码:
qpixmap p1, p2;p1.load(image.bmp);p2 = p1;                        // p1 和 p2 共享数据qpainter paint;paint.begin(&p2);               // 将p2从p1中分离出来paint.drawtext(0,50, iriczhao);paint.end();  
注:在使用stl风格的迭代器时,复制隐式共享容器(qmap,qlist等)需要特别注意。
五、隐式共享迭代器问题
对于stl风格的迭代器,在使用隐式共享类时应格外注意。因为当迭代器在容器上激活时,应该避免复制容器。也就是迭代器指向一个内部结构,如果复制一个容器,此时应特别注意迭代器。例如以下代码片段:
qlist a, b;a.resize(100000); // 创建一个大列表,里面填满0。qlist::iterator i = a.begin();/*-------------------------------------------------------------*/// 使用迭代器i的错误方法:b = a;/*    此时我们应该注意迭代器i,因为它将指向共享数据    如果我们执行*i = 4,那么我们将改变共享实例(两个向量)    其行为不同于stl容器。在qt中不能这样做。*//*-------------------------------------------------------------*/a[0] = 5;/*    容器a现在与共享数据分离,    尽管i是容器a的迭代器,但是它现在作为容器b的迭代器工作。    这里的情况是(*i) == 0。*/b.clear(); // 现在迭代器i完全无效了。int j = *i; //此时会出现未定义的行为!/*    来自b(i所指向的)的数据不见了。    这可以用stl容器(和(*i) == 5)定义,    但是这时候使用qlist,可能会崩溃。*/  
总而言之:当迭代器在容器上激活时,应该避免复制容器,所有的qt容器类都应该注意这一点。
六、隐式共享类和线程
在qt中,对它的许多值类使用了隐式共享进行了优化,尤其是qimage和qstring。从qt 4开始,隐式共享类可以安全地跨线程复制。这些值类是完全可重入的。
一般情况下,都认为隐式共享和多线程是不兼容的概念,因为引用计数通常不允许这样做。然而,qt使用原子引用计数来确保共享数据的完整性,避免了引用计数器的潜在损坏。
但是需要注意原子引用计数不能保证线程安全性。在线程之间共享隐式共享类的实例时,应该适当的加锁进行锁定。这一点,与所有重入类(无论是否共享)相同。原子引用计数确实保证了一个线程在其自身、隐式共享类的本地实例上工作是安全的,所以,在开发中可以使用信号和槽函数机制在不同线程之间传递数据,因为这可以在不需要显式锁定的情况下完成。
总而言之,qt 中的隐式共享类实际上是隐式共享的。即使在多线程应用程序中,也可以安全地使用它们,与普通的、非共享的、可重入的基于值的类一样。


华为官方已经开启畅享9手机的预约活动,官方表示该机“千元实力派”
半导体景气不会二次衰退
有哪些人工智能
裸眼技术为3D电视发展开创新里程碑
南卡S1蓝牙耳机评测 外观时尚设计精致材质质感精益求精
基于Qt 5.15源码来聊聊隐式共享
捷德移动安全联手M2MD:推出更加安全的车辆移动访问解决方案
折叠屏手机初体验,换机该选谁?iPhone不跟进的理由是什么?
红外热成像仪器介绍
JY44B系列直流电阻测试仪的技术特点
单极性PWM调制和双极性PWM调制有什么区别
京东健康已获准在港上市
华为P10和华为荣耀v9哪个好?华为P10和华为荣耀v9区别评测对比
智能家居魔镜的到来标志着智慧时代已来临
大数据量处理和传输的增长将提高对连接器的要求
联电可能100亿新台币收购东芝8英寸晶圆厂
NI LabVIEW软件助力2008北京奥运会比赛场地建筑健
方正IT利用生物识别术,实现轨交新型支付手段
腾讯加速了远程办公等线上服务的落地,推动了云计算市场的快速发展
电瓶修复技术—这俩种电池的差距大吗