C++隐式共享与Qt的D指针

一、动态库的二进制兼容

库的编译链接分为静态与动态,静态的优点是可执行文件在运行的时候不依赖于动态链接库,而缺点是浪费空间和程序更新文件较大;动态的优点是多个可执行程序可共享库,从而节约空间,但在动态库更新时,容易引入二进制不兼容的问题。

C++中,接口一般是由头文件和library二进制代码提供,因此,任何可能造成library代码和旧的头文件不一致的情况都可能破坏二进制兼容。

结果

允许操作

不会破坏动态库二进制兼容

添加新的非虚函数,包括信号、槽和构造函数

在类中添加新的枚举

删除没有任何内联函数调用的私有非虚函数

删除没有任何内联函数调用的私有静态成员

添加新的静态数据成员

添加新类

导出以前未导出的类

如果类已经从QObject继承,则将Q_OBJECT宏添加到类中

添加Q_PROPERTY,Q_ENUMS或Q_FLAGS宏,因为它只修改由moc生成的元对象而不是类本身

破坏动态库二进制兼容

取消导出或删除导出的类

以任何方式更改类层次结构(添加、删除或重新排序基类)

将普通函数变为内联函数(这包括将成员函数的主体移动到类定义,即使没有内联关键字)

更改函数参数列表中任何参数的类型

更改函数的const / volatile限定符

更改函数的的返回值类型

将虚函数添加到没有任何虚函数的类中

更改类声明中的虚函数的顺序

删除虚函数,即使它是从基类重新实现虚函数

将新的非静态数据成员添加到现有类

更改类中非静态数据成员的顺序

更改成员的类型

从现有类中删除非静态数据成员

C++库可以利用d-pointer技术,将本应属于公类的数据成员剥离出来放置到另一个私类中。更新库源代码时,开发者可以自由变更私类中的数据而不必担心破坏库的二进制兼容性。


二、PIMPL

通常情况下,与一个类密切相关的数据会被作为数据成员直接定义在该类中。然而,在某些场合下,我们会将这些数据从该类(被称为公类)分离出来,定义在一个单独的类中(被称为私类)。公类中会定义一个指针,指向私类的对象。在计算机的发展历史中,这种模式被称为pointer to implementation(pimpl),或者handle/body或者cheshire cat柴郡猫逐渐消失,什么也没有留下,除了它的微笑。而在这里,留下的变成了实现指针)。

1.信息隐蔽

私有成员完全可以隐藏在共有接口之外,尤其对于闭源API的设计尤其的适合。同时,很多代码会应用平台依赖相关的宏控制,这些琐碎的东西也完全可以隐藏在实现类当中,给用户一个间接明了的使用接口再好不过了。

2.加速编译

这通常是用pImpl手法的最重要的收益,称之为编译防火墙(compilation firewall),主要是阻断了类的实现和类的实现两者的编译依赖性。这样,类用户不需要额外include不必要的头文件,同时实现类的成员可以随意变更,而公有类的使用者不需要重新编译。

3.更好的二进制兼容性

承接上面说的,通常对一个类的修改,会影响到类的大小、对象的表示和布局等信息,那么任何该类的用户都需要重新编译才行。而且即使更新的是外部不可访问的private部分,虽然从访问性来说此时只有类成员和友元能否访问类的私有部分,但是由于C++的特性是名字查找先于名字查找和重载解析的(即使不可访问也会返回调用失败,而不是视而不见),私有部分的修改也会影响到类使用者的行为,这也迫使类的使用者需要重新编译。而对于使用pImpl手法,如果实现变更被限制在实现类中,那公有类只持有一个实现类的指针,所以实现做出重大变更的情况下,pImpl也能够保证良好的二进制兼容性。

4.拷贝语义

pImpl最需要关注的就是公有类的复制语义,因为实现类是以指针的方式作为公有类的一个成员,而默认C++生成的拷贝操作只会执行对象的浅复制,这显然违背了pImpl的原本意图,除非是真的想要底层共享一个实现对象。针对这个问题,解决方式有:

  • 禁止复制操作,将所有的复制操作定义为private的,或者在新标准中将这些复制操作定义为delete的即可
  • 显式定义复制语义,创建新的实现类对象,执行深度复制操作。

5.不足

  • 该手法需要在调用和实现之间插入了一个指针,公有类在访问私有成员的时候都需要增加mImpl->前缀的方式,使用、阅读和调试都可能有所不便;
  • pImpl对拷贝操作比较敏感,要么禁止拷贝操作,要么就需要自定义拷贝操作;
  • 编译器将不再能够捕获const方法中对成员变量的修改,因为私有成员变量已经从公有类脱离到了实现类当中了,公有类的const只能保护指针值本身是否改变,而不再能进一步保护其所指向的数据。

三、Qt的宏

Qt通过定义宏的方式来让PIMPL的使用更加简单,主要包括Q_DECLARE_PRIVATEQ_DECLARE_PUBLICQ_DQ_Q,其定义和依赖函数如下所示:

1.依赖的函数

template <typename T> static inline T *qGetPtrHelper(T *ptr) { return ptr; }

template <typename Wrapper> static inline typename Wrapper::pointer qGetPtrHelper(const Wrapper &p) { return p.data(); }

2.Q_DECLARE_PRIVATE

#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;

在公类中定义一个成员函数d_func,返回一个指针,指向对应的私类。

3.Q_DECLARE_PUBLIC

#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;

在私类中定义一个成员函数q_func,返回一个指针,指向对应的公类。

4.Q_D

#define Q_D(Class) Class##Private * const d = d_func()

将成员函数d_func() 返回的指针重命名为更加简洁的”d”

5.Q_Q

#define Q_Q(Class) Class * const q = q_func()

将成员函数q_func() 返回的指针重命名为更加简洁的”q”

总的来说,一方面,Qt在公类中定义了一个指针d_ptr指向私类,在宏Q_DECLARE_PRIVATE中定义了一个函数获取这个指针,用宏Q_D将这个指针重新命名为d,以便于访问私类对象。另一方面,Qt在私类中定义了一个指针q_ptr指向公类,在宏Q_DECLARE_PUBLIC中定义了一个函数获取这个指针,用宏Q_Q将这个指针重新命名为q,以便于访问公类对象。


四、隐式共享

一般情况下,一个类的多个对象所占用的内存是相互独立的。如果其中某些对象数据成员的取值完全相同,我们可以令他们共享一块内存以节省空间。只有当程序需要修改其中某个对象的数据成员时,我们再为该对象分配新的内存。这种技术被称为隐式共享(implicit sharing)。QT中所有的容器类都支持隐式共享,以及QByteArray, QPalette,QBitmap,QImage,QPixmap,QCursor,QDir,QFontQVariant等也都支持隐式共享机制。

隐式共享(implicit sharing)的目的在于节省内存、提高程序运行速度。如下图所示,设对象O1O2O3的数据成员具有相同的取值。为了节省内存,我们用一个内存块A来存放这些数据成员,每个对象内部有一个指针,指向这个内存块。设此时有一个O3来复制构造O4的操作。由于此时O4O3具有相同的数据成员,所有O4页可以和O1~O3共享内存块A。此后,程序请求修改O4的数据成员。由于逻辑上O1O2O3O4是相互独立的对象,所以我们不能够直接修改内存块A中的数据,否则,会影响O1~O3的数据。内存块A中的数据被复制到一个新的内存块B,对O4的修改会施加到内存块B中存放的数据。这就是所谓的“写时复制(copy-on-write)”名称的来源。这种技术能够在逻辑上保证各个对象是相互独立的。同时,在物理实现上,只要某些对象的数据成员值相同,则它们就会共享内存,以节省内存资源。

 

除了能够节省内存之外,这种技术还可以提高程序运行速度。设想我们需要用O3来构造一个对象O5.如果O3O5各自使用独立的内存块存放数据,则这个构造操作需要将O3中的数据完全复制到O5中,这需要较长的运行时间。而采用了隐式共享技术后,只需要设置O5中的一个指针,以指向被共享的数据块,这个操作的执行速度会很快。

当然,采用这种技术需要一些额外的内存管理工作。在上图中,设想客户要析构对象O3。我们不能够简单的将O3所用的内存块A释放,因为O1O2还在使用使用这个内存块。当只有一个对象使用内存块A时,在析构该对象时才可以释放释放内存块A。为此,我们需要维护一个引用计数(reference counter)。每当有一个新的对象需要共享该内存块时,该内存块对应的引用计数被加1,每当共享该内存块的一个对象被析构时,该引用计数被减1.如果减1后为0,则说明已经没有任何对象需要使用该内存块,该内存块被释放。

直接定义一个类的数据成员无法实现隐式共享,因为每创建一个对象都会生成一份自己的数据,为了能够共享数据,我们只能采用PIMPL方式。

而对于Qt程序,Qt为我们提供了更加简单的方式实现隐式共享,即直接使用QSharedDataQSharedDataPointer这两个类来实现我们自己的隐式共享。


五、显示共享

与隐式共享对应的,如果我们在XX类中的声明d指针时使用的是QExplicitlySharedDataPointer<XXPrivate>,那么就是使用了显式共享,写时复制操作就不会自动发生了,所有对象将共享相同的数据。

 

 

 

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值