PIMPL机制 – Qt源码技术解析
目录
背景🌏
最近因项目需要开始接触Qt开发,在查看源码时,发现了很多的类头文件里面都出现了Q_DECLARE_PRIVATE
这样的宏,如下面Qt里面QCoreApplication
类的代码示例。
class Q_CORE_EXPORT QCoreApplication : public QObject
{
Q_DECLARE_PRIVATE(QCoreApplication)
public:
QCoreApplication(int &argc, char **argv, int = ApplicationFlags);
~QCoreApplication();
// 这里省略其他内容
...
};
之后研究了相关资料,了解到了Qt源码使用了一种叫PIMPL(Pointer to Implementation)的技巧,即指针指向实现。其方式是把一个类的实现细节从类的声明定义中移除,把它们放到另一个分离出来的私有类中,并把私有类放进源文件中。原先的类对这个私有类进行前向声明并通过一个私有类指针来访问具体的实现细节,从而实现对外屏蔽类中私有属性和私有方法的目的。
这种做法在构建C++库时,可以提供稳定的ABI接口,减少编译时的依赖和耦合。
举个栗子🌰
我们用下边的FooService
举个例子,假设FooService
用于给外部提供一些服务,包括ServiceA
、ServiceB
。在打包成库后,最终得到一个库和一个头文件。
我们用两种实现方式来进行下对比:
未用PIMPL示例:
// FooService.h
#pragma once
class FooService
{
public:
void ServiceA();
void ServiceB();
private:
bool Init();
private:
bool service_ready{false};
};
// FooService.cpp
#include "FooService.h"
void FooService::ServiceA()
{
if (!Init()) return;
// concrete implementation of ServiceA
}
void FooService::ServiceB()
{
if (!Init()) return;
// concrete implementation of ServiceB
}
bool FooService::Init()
{
service_ready = true;
return service_ready;
}
这种实现方式有什么缺点?
- 头文件
FooService.h
对外暴露了私有的成员。当一个类很庞大复杂时,接口文件中出现大量跟接口使用无关的实现细节,会使得对外的接口文件变得复杂,不清晰。 - 实现和接口耦合在一起,违反开闭原则(Open Closed Principle)。修改实现就会影响到整个接口类。
- 模块编译变得耦合。比如我们要修改一下
FooService
类,在头文件中增加一个私有成员,这时候就会导致所有使用这个库的用户都需要重新编译他们的代码。
使用PIMPL示例:
// FooService.h
#pragma once
class FooServicePrivate;
class FooService
{
public:
void ServiceA();
void ServiceB();
private:
FooServicePrivate* pimpl_;
};
// FooService.cpp
#include "FooService.h"
void FooService::ServiceA()
{
pimpl_->ServiceB();
}
void FooService::ServiceB()
{
pimpl_->ServiceA();
}
class FooServicePrivate
{
private:
bool service_ready{false};
private:
bool Init();
public:
void ServiceA();
void ServiceB();
};
bool FooServicePrivate::Init()
{
service_ready = true;
return service_ready;
}
void FooServicePrivate::ServiceA()
{
if (!Init()) return;
// concrete implementation of ServiceA
}
void FooServicePrivate::ServiceB()
{
if (!Init()) return;
// concrete implementation of ServiceB
}
使用PIMPL的优点:
- 接口和实现分离,降低耦合度
- 降低了编译时模块间的依赖,提高了其他依赖模块的编译速度
Qt PIMPL机制的实现
以为QCoreApplication
例,我们对相关的宏进行展开。
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;
原始的QCoreApplication
:
class QCoreApplicationPrivate;
class Q_CORE_EXPORT QCoreApplication : public QObject
{
Q_DECLARE_PRIVATE(QCoreApplication)
public:
QCoreApplication(int &argc, char **argv, int = ApplicationFlags);
~QCoreApplication();
...
};
展开Q_DECLARE_PRIVATE
宏后:
class QCoreApplicationPrivate;
class Q_CORE_EXPORT QCoreApplication : public QObject
{
inline QCoreApplicationPrivate* d_func() {
return reinterpret_cast<QCoreApplicationPrivate*>(qGetPtrHelper(d_ptr));
}
inline const QCoreApplicationPrivate* d_func() const {
return reinterpret_cast<QCoreApplicationdPrivate*>(qGetPtrHelper(d_ptr));
}
friend class QCoreApplicationPrivate;
...
};
可以看到,展开后出现了两个d_func()
方法和一个友元类QCoreApplicationPrivate
。
进一步展开qGetPtrHelper
方法后:
// 这是一个模板方法,如果只是一个普通的类指针,则返回该指针;
// 而如果是一个类模板方法,则调用data()方法,并返回结果
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(); }
// 展开qGetPtrHelper:
class Q_CORE_EXPORT QCoreApplication : public QObject
{
inline QCoreApplicationPrivate* d_func() {
return reinterpret_cast<QCoreApplicationPrivate*>(d_ptr.data());
}
inline const QCoreApplicationPrivate* d_func() const {
return reinterpret_cast<QCoreApplicationdPrivate*>(d_ptr.data());
}
friend class QCoreApplicationPrivate;
...
};
可以看到d_func()
方法里面返回的是一个指针d_ptr
,那么这个指针是从哪里来的?
我们在QCoreApplication
类中没有找到,但是在它的父类QObject
中发现了它的身影,且属于protected属性,可以被友元类访问,这里也呼应了上面宏张开后有一个友元类的原因:
class Q_CORE_EXPORT QObject
{
...
protected:
QScopedPointer<QObjectData> d_ptr;
};
但是这个指针的类型是QObjectData
的,为什么可以被我们用reinterpret_cast
强制转换为QCoreApplicationdPrivate*
类型呢?
我们查看它们的源码,发现它们之间存在着继承关系QCoreApplicationPrivate
-> QObjectPrivate
-> QObjectData
,这也解释了为什么可以进行强制转换:
// https://codebrowser.dev/qt5/qtbase/src/corelib/kernel/qobject_p.h.html
class Q_CORE_EXPORT QObjectPrivate : public QObjectData
{
}
// https://codebrowser.dev/qt5/qtbase/src/corelib/kernel/qcoreapplication_p.h.html
class Q_CORE_EXPORT QCoreApplicationPrivate : public QObjectPrivate
{
}
通过以上分析,我们就可以知道Q_DECLARE_PRIVATE
宏最终目的就是为了可以得到指向私有类的函数指针,也就是Qt中常说的d
指针。
那么在类里面有了私有类的d
指针后,我们怎么使用它?
有两种方式:
// 1. 直接使用 d_func()
QCoreApplicationPrivate *d = d_func();
return d->cachedApplicationDirPath;
// 2. 使用Qt自带的Q_D宏,展开后就是通过 d_func() 获取到d指针
// #define Q_D(Class) Class##Private * const d = d_func()
Q_D(QCoreApplication);
return d->cachedApplicationDirPath;
以QCoreApplication
的源码中的接口applicationDirPath
为例,可以看到最终有调用到私有类的cachedApplicationDirPath
成员。
QString QCoreApplication::applicationDirPath()
{
...
QCoreApplicationPrivate *d = self->d_func();
if (d->cachedApplicationDirPath.isNull())
d->cachedApplicationDirPath = QFileInfo(applicationFilePath()).path();
return d->cachedApplicationDirPath;
}
以上,就是整个Qt PIMPL机制的实现过程,在类中通过Q_DECLARE_PRIVATE
定义私有类指针,接着通过私有类指针访问具体的实现细节。
QT PIMPL机制的使用
那我们平时写代码时可以怎么使用Qt的PIMPL机制呢?还是以上面的FooService
来举例说明:
-
如果是非Qt平台,可以把PIMPL用到的宏拷贝到自己的平台代码里:
global.h
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(); } #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_D(Class) Class##Private * const d = d_func()
-
头文件中包含类
FooService.h
// FooService.h #pragma once class FooServicePrivate; class FooService { Q_DECLARE_PRIVATE(FooService) public: FooService(); ~ FooService(); void ServiceA(); void ServiceB(); protected: QScopedPointer<FooServicePrivate> d_ptr; };
-
源文件中声明私有类,同时定义公开类和私有类的实现。(也可以把私有类的声明放到另一个头文件中,再包含进来)
FooService.cpp
下边屏蔽的部分属于q指针的内容,这里只做补充,不细讲。
// FooService.cpp #include "FooService.h" FooService::FooService() : d_ptr(new FooServicePrivate) { // d_ptr->q_ptr = this; // q 指针赋值 } FooService() { delete d_ptr; } void FooService::ServiceA() { d_ptr->ServiceB(); } void FooService::ServiceB() { d_ptr->ServiceA(); } class FooServicePrivate { // 如果希望私有类可以访问公有类的数据,可以使用该宏,展开后会得到 q 指针 // Q_DECLARE_PUBLIC(FooService) protected: // FooService* q_ptr; private: bool service_ready{false}; private: bool Init(); public: void ServiceA(); void ServiceB(); }; bool FooServicePrivate::Init() { service_ready = true; return service_ready; } void FooServicePrivate::ServiceA() { if (!Init()) return; // concrete implementation of ServiceA } void FooServicePrivate::ServiceB() { if (!Init()) return; // concrete implementation of ServiceB }
补充 q 指针相关:
上面已知,公有类通过d_func()
(d
指针)来访问私有类ClassPrivate
的数据;反过来,如果我们想让ClassPrivate
访问公有类的数据,就可以q_func()
(q
指针)。
#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;
#define Q_Q(Class) Class * const q = q_func()
参考链接
原文链接:https://juejin.cn/post/7227084645480120375 作者:陈俊任