Qt d_pointer
什么是d_pointer
如果你曾经查看过Qt的源代码文件,例如这个Q_D和Q_Q宏定义。
文就来揭开这些宏使用的目的。
Q_D 和Q_Q宏定义是d-pointer, 它可以把一个类库的实施细节对使用的用户隐藏, 而且对实施的更改不会打破二进制兼容。
什么是二进制兼容
在设计像 Qt 这样的类库的时候,理想的行为应该是动态连接到 Qt 的应用程序,甚至在 Qt 类库升级或者替换到另外一个版本的时候,不需要重新编译就可以继续运行。
什么会打破二进制兼容
那么,什么时候类库的变化需要应用程序的重新编译呢? 我们来看一个简单的例子:
我们需要新建一个项目WidgetLib
cmake_minimum_required(VERSION 3.19) project(WidgetLib) set(CMAKE_CXX_STANDARD 17) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY_DEBUG ${PROJECT_SOURCE_DIR}/../output) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY_RELEASE ${PROJECT_SOURCE_DIR}/../output) set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_DEBUG ${PROJECT_SOURCE_DIR}/../output) set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_RELEASE ${PROJECT_SOURCE_DIR}/../output) set(QT_VERSION 5) set(REQUIRED_LIBS Core Gui Widgets PrintSupport) set(REQUIRED_LIBS_QUALIFIED Qt5::Core Qt5::Gui Qt5::Widgets Qt5::PrintSupport) find_package(Qt${QT_VERSION} COMPONENTS ${REQUIRED_LIBS} REQUIRED) #自动查找头文件路径函数 macro(FIND_INCLUDE_DIR result curdir) #定义函数,2个参数:存放结果result;指定路径curdir; file(GLOB_RECURSE children "${curdir}/*.hpp" "${curdir}/*.h" ) #遍历获取{curdir}中*.hpp和*.h文件列表 file(GLOB SOURCE_INCLUDE ${children} ) #将文件放入 SOURCE_INCLUDE 中 set(dirlist "") #定义dirlist中间变量,并初始化 foreach(child ${children}) #for循环 string(REGEX REPLACE "(.*)/.*" "\\1" LIB_NAME ${child}) #字符串替换,用/前的字符替换/*h if(IS_DIRECTORY ${LIB_NAME}) #判断是否为路径 list (FIND dirlist ${LIB_NAME} list_index) #判断dirlist是否含有${LIB_NAME} if(${list_index} LESS 0) LIST(APPEND dirlist ${LIB_NAME}) #将合法的路径加入dirlist变量中 else() endif() #结束判断 endif() endforeach() #结束for循环 set(${result} ${dirlist}) #dirlist结果放入result变量中 endmacro() #自动查找源文件路径函数 macro(FIND_SRC_DIR result curdir) file(GLOB_RECURSE children "${curdir}/*.cpp" "${curdir}/*.cc" "${curdir}/*.cxx") file(GLOB SOURCE_SRC ${children} ) set(dirlist "") foreach(child ${children}) string(REGEX REPLACE "(.*)/.*" "\\1" LIB_NAME ${child}) if(IS_DIRECTORY ${LIB_NAME}) list (FIND dirlist ${LIB_NAME} list_index) if(${list_index} LESS 0) LIST(APPEND dirlist ${LIB_NAME}) else() endif() endif() endforeach() set(${result} ${dirlist}) endmacro() #调用函数,指定参数 #自动查找头文件路径函数 macro(FIND_UI_DIR result curdir) #定义函数,2个参数:存放结果result;指定路径curdir; file(GLOB_RECURSE children "${curdir}/*.ui") #遍历获取{curdir}中*.hpp和*.h文件列表 file(GLOB SOURCE_UI ${children} ) #将文件放入 SOURCE_INCLUDE 中 set(dirlist "") #定义dirlist中间变量,并初始化 foreach(child ${children}) #for循环 string(REGEX REPLACE "(.*)/.*" "\\1" LIB_NAME ${child}) #字符串替换,用/前的字符替换/*h if(IS_DIRECTORY ${LIB_NAME}) #判断是否为路径 list (FIND dirlist ${LIB_NAME} list_index) #判断dirlist是否含有${LIB_NAME} if(${list_index} LESS 0) LIST(APPEND dirlist ${LIB_NAME}) #将合法的路径加入dirlist变量中 else() endif() #结束判断 endif() endforeach() #结束for循环 set(${result} ${dirlist}) #dirlist结果放入result变量中 endmacro() FIND_SRC_DIR(SRC_DIR_LIST ${PROJECT_SOURCE_DIR}) FIND_INCLUDE_DIR(INCLUDE_DIR_LIST ${PROJECT_SOURCE_DIR}) FIND_UI_DIR(UI_DIR_LIST ${PROJECT_SOURCE_DIR}) #将INCLUDE_DIR_LIST中路径列表加入工程,包括第三方库的头文件路径 include_directories( ${INCLUDE_DIR_LIST} #INCLUDE_DIR_LIST路径列表加入工程 ) add_compile_options("$<$<C_COMPILER_ID:MSVC>:/utf-8>") add_compile_options("$<$<CXX_COMPILER_ID:MSVC>:/utf-8>") #add_executable(${PROJECT_NAME} WIN32 main.cpp hello.qrc # ${SOURCE_INCLUDE} ${SOURCE_SRC} ${SOURCE_UI}) add_library(${PROJECT_NAME} SHARED ${SOURCE_INCLUDE} ${SOURCE_SRC} ${SOURCE_UI}) #set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>") set_target_properties(${PROJECT_NAME} PROPERTIES CMAKE_MSVC_RUNTIME_LIBRARY_RELEASE "MultiThreaded$<$<CONFIG:Release>:Release>") target_link_libraries(${PROJECT_NAME} ${REQUIRED_LIBS_QUALIFIED})
WidgetLib.h
#ifndef AUTOTESTTEST_WIDGETLIB_H #define AUTOTESTTEST_WIDGETLIB_H #include <QRect> #include <QString> #include <string> #include <qglobal.h> class Q_DECL_EXPORT WidgetLib { public: WidgetLib(); ~WidgetLib(); private: QRect m_geometry; }; #endif //AUTOTESTTEST_WIDGETLIB_H
WidgetLib.cpp
#include "WidgetLib.h" WidgetLib::WidgetLib() { } WidgetLib::~WidgetLib() { }
我们需要再新建一个项目:
只需要修改上面项目的名称即可:
WidgetLabel.h
#ifndef WIDGETLIB_WIDGETLABEL_H #define WIDGETLIB_WIDGETLABEL_H #include <QString> #include "WidgetLib.h" #include <qglobal.h> class Q_DECL_EXPORT WidgetLabel : public WidgetLib { public: WidgetLabel(); ~WidgetLabel(); QString text() const { return m_text; } private: QString m_text{"im widget label."}; }; #endif //WIDGETLIB_WIDGETLABEL_H
WidgetLabel.cpp
#include "WidgetLabel.h" WidgetLabel::WidgetLabel() { } WidgetLabel::~WidgetLabel() { }
在最外部的项目main中我们这么写:
main.cpp
#include <QApplication> #include <iostream> #include "WidgetLabel/WidgetLabel.h" int main(int argc, char **argv) { QApplication app(argc, argv); WidgetLabel label; std::cout << label.text().toStdString() << std::endl; return app.exec(); }
这个时候我们可以正常输出text。
如果此时有人执意在WidgetLib中添加一个参数
WidgetLabel.h
#ifndef AUTOTESTTEST_WIDGETLIB_H #define AUTOTESTTEST_WIDGETLIB_H #include <QRect> #include <QString> #include <string> #include <qglobal.h> class Q_DECL_EXPORT WidgetLib { public: WidgetLib(); ~WidgetLib(); private: QRect m_geometry; QString ccc; }; #endif //AUTOTESTTEST_WIDGETLIB_H
这个时候我们的程序光荣的崩溃了。
为什么会崩溃
究其原因,通过添加了一个新的数据成员,我们最终改变了 WidgetLib 和 WidgetLabel 对象的大小。为什么会这样?因为当你的C+编译器生成代码的时候,他会用偏移量来访问对象的数据。
下面是一个 POD 对象在内存里面布局的一个简化版本。
WidgetLabel对象在 WidgetLib 1.0 | WidgetLabel对象在 WidgetLib 1.1 |
m_geometry <offset 0> | m_geometry <offset 0> |
- - - | m_stylesheet <offset 1> |
m_text <offset 1> | - - - |
- - - | m_text <offset 2> |
在 WidgetLib 1.0中,WidgetLabel的 text 成员在(逻辑)偏移量为1的位置。在编译器生成的代码里,应用程序的方法 Label::text() 被翻译成访问 Label 对象里面偏移量为1的位置。 在 WidgetLib 1.1中,WidgetLabel的 text 成员的(逻辑)偏移量被转移到了2的位置!由于应用程序没有重新编译,它仍然认为 text 在偏移量1的位置,结果却访问了 stylesheet 变量!
我确信,这个时候,会有人问,为什么Label::text()的偏移量的计算的代码会在App二进制文件结束,而不是在WidgetLib的二进制文件。 答案是因为Label::text() 的代码定义在头文件里面,最终被内联
那么,如果 Label::text() 没有定义为内联函数,情况会改变吗?这么讲,Label::text() 被移到源文件里面?嗯,不会。C编译器依赖对象大小在编译时和运行时相同。比如,堆栈的 winding/unwinding - 如果你在堆栈上创建了一个 WidgetLabel对象, 编译器产生的代码会根据 WidgetLabel对象在编译时的大小在堆栈上分配空间。由于WidgetLabel的大小在 WidgetLib 1.1 运行时已经不同,WidgetLabel的构造函数会覆盖已经存在的堆栈数据,最终破坏堆栈。
不要改变导出的 C++类的大小
总之,一旦你的类库发布了,永远不要改变 导出的 C++ 类的大小或者布局(不要移动成员)。C++ 编译器生成的代码会假定,一个类的大小和成员的顺序 编译后*就不会改变.
那么,如何在不改变对象的大小的同时添加新的功能呢?
d-pointer
诀窍是通过保存唯一的一个指针而保持一个类库所有公共类的大小不变。这个指针指向一个包含所有数据的私有的(内部的)数据结构。内部结构的大小可以增大或者减小,而不会对应用程序带来副作用,因为指针只会被类库里面的代码访问,从应用程序的视角来看,对象的大小并没有改变 - 它永远是指针的大小。 这个指针被叫做
d-pointer 。
这个模式的精髓可以由下面的代码来概述(本文中的所有代码都没有析构函数,在实际使用的时候应该加上它)。
WidgetLib.h
#ifndef AUTOTESTTEST_WIDGETLIB_H #define AUTOTESTTEST_WIDGETLIB_H #include <QRect> #include <QString> #include <string> #include <qglobal.h> class WidgetPrivate; class Q_DECL_EXPORT WidgetLib { public: WidgetLib(); ~WidgetLib(); QRect geometry() const; private: WidgetPrivate *d_ptr; }; #endif //AUTOTESTTEST_WIDGETLIB_H
WidgetLib.cpp
#include "WidgetLib.h" #include "WidgetLib_p.h" WidgetLib::WidgetLib() : d_ptr(new WidgetPrivate) { } WidgetLib::~WidgetLib() { } QRect WidgetLib::geometry() const { return d_ptr->geometry; }
WidgetLib_p.h
#ifndef WIDGETLIB_WIDGETLIB_P_H #define WIDGETLIB_WIDGETLIB_P_H #include <QRect> #include <QString> struct WidgetPrivate { QRect geometry; QString stylesheet; }; #endif //WIDGETLIB_WIDGETLIB_P_H
有了上面的机构,App 从来不需要直接访问 d-pointer。由于 d-pointer 只是在 WidgetLib 被访问,而 WidgetLib 在每一次发布都被重新编译,私有的类可以随意的改变而不会对 App 带来影响。
d-pointer 的其它好处
这里不全都是和二进制兼容有关。d-pointer 还有其它的好处: 隐藏了实现细节 - 我们可以只发布带有头文件和二进制文件的 WidgetLib。源文件可以是闭源代码的。
头文件很干净,不包含实现细节,可以直接作为 API 参考。由于实施需要的包含的头文件从头文件里已到了实施(源文件)里面,编译速更快。(译:降低了编译依赖)
事实上,上边的好处是微乎其微的。Qt 使用 d-pointer 的真正原因是为了二进制兼容和 Qt 最初是封闭源代码的
q-pointer
到目前为止,我们仅仅看到的是作为 C 风格的数据机构的 d-pointer。实际上,它可以包含私有的方法(辅助函数)。例如,LabelPrivate 可以有一个getLinkTargetFromPoint() 辅助函数,当鼠标点击的时候找到目标链接。在很多情况下,这些辅助函数需要访问公有类,也就是 WidgetLabel 或者它的父类 WidgetLib 的一些函数。比如,一个辅助函数 setTextAndUpdateWidget() 想要调用一个安排重画WidgetLib 的公有方法Widget::update()。所以,WidgetPrivate 存储了一个指向公有类的指针,称为q-pointer。修改上边的代码引入q-pointer,我们得到下面代码:
WidgetLib
WidgetLib.h #ifndef AUTOTESTTEST_WIDGETLIB_H #define AUTOTESTTEST_WIDGETLIB_H #include <QRect> #include <QString> #include <string> #include <qglobal.h> class WidgetPrivate; class Q_DECL_EXPORT WidgetLib { public: WidgetLib(); WidgetLib(WidgetPrivate &d); ~WidgetLib(); QRect geometry() const; protected: WidgetPrivate *d_ptr; private: }; #endif //AUTOTESTTEST_WIDGETLIB_H WidgetLib.cpp #include "WidgetLib.h" #include "WidgetLib_p.h" WidgetLib::WidgetLib() : d_ptr(new WidgetPrivate(this)) { } WidgetLib::WidgetLib(WidgetPrivate &d) : d_ptr(&d) { } WidgetLib::~WidgetLib() { } QRect WidgetLib::geometry() const { return d_ptr->geometry; } WidgetLib_p.h #ifndef WIDGETLIB_WIDGETLIB_P_H #define WIDGETLIB_WIDGETLIB_P_H #include <QRect> #include <QString> #include "WidgetLib.h" struct WidgetPrivate { WidgetPrivate(WidgetLib *q) : q_ptr(q) { } WidgetPrivate() {} WidgetLib *q_ptr; // q-ptr that points to the API class QRect geometry; QString stylesheet; QString aaa; QString nnn; }; #endif //WIDGETLIB_WIDGETLIB_P_H
WidgetLabel
WidgetLabel.h #ifndef WIDGETLIB_WIDGETLABEL_H #define WIDGETLIB_WIDGETLABEL_H #include <QString> #include "WidgetLib.h" #include <qglobal.h> class LabelPrivate; class Q_DECL_EXPORT WidgetLabel : public WidgetLib { public: WidgetLabel(); ~WidgetLabel(); QString text() const; protected: WidgetLabel(LabelPrivate &d); }; #endif //WIDGETLIB_WIDGETLABEL_H WidgetLabel.cpp #include "WidgetLabel.h" #include "WidgetLib_p.h" struct LabelPrivate : public WidgetPrivate { LabelPrivate(WidgetLabel *q) : q_ptr(q) { } WidgetLabel *q_ptr; QString text{"im a label"}; }; WidgetLabel::WidgetLabel() : WidgetLib(*new LabelPrivate(this)) { } WidgetLabel::WidgetLabel(LabelPrivate &d) : WidgetLib(d) { } WidgetLabel::~WidgetLabel() { } QString WidgetLabel::text() const { LabelPrivate *d = static_cast<LabelPrivate*>(d_ptr); return d->text; }
是不是很漂亮?现在当我们创建一个 WidgetLabel 对象时,它会创建一个 LabelPrivate(它继承了WidgetPrivate)。它把一个 d-pointer 实体传递给Widget的保护的构造函数。WidgetLabel 也有这样一个保护的构造函数,可以被继承 Label 的类提供自己的私有类来使用。
Q_D和Q_Q
在text()中执行了一个强转才可以运行。
代码里到处都是 static_cast 看起来不是那么漂亮,所以QT定义了下面的宏
#define Q_D(Class) Class##Private * const d = d_func() #define Q_Q(Class) Class * const q = q_func()
在使用的时候可以按照下面代码使用:
// With Q_D you can use the members of LabelPrivate from Label void Label::setText(const String &text) { Q_D(Label); d->text = text; } // With Q_Q you can use the members of Label from LabelPrivate void LabelPrivate::someHelperFunction() { Q_Q(Label); q->selectAll(); }
Q_DECLARE_PRIVATE与Q_DECLARE_PUBLIC是Qt用来在类的头文件中声明获取d指针与q指针的私有函数,其核心在于添加了强制类型转换。
Q_D与Q_Q两个宏是用来获取d/q常量指针的,在函数中可以直接使用d变量或者q变量代替d_ptr与q_ptr,因为通过它们获取的指针类型是具体的,所以是直接使用ptr变量代替不了的。
qglobal.h
// The body must be a statement: #define Q_CAST_IGNORE_ALIGN(body) QT_WARNING_PUSH QT_WARNING_DISABLE_GCC("-Wcast-align") body QT_WARNING_POP #define Q_DECLARE_PRIVATE(Class) \ inline Class##Private* d_func() \ { Q_CAST_IGNORE_ALIGN(return reinterpret_cast<Class##Private *>(qGetPtrHelper(d_ptr));) } \ inline const Class##Private* d_func() const \ { Q_CAST_IGNORE_ALIGN(return reinterpret_cast<const Class##Private *>(qGetPtrHelper(d_ptr));) } \ friend class Class##Private; #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; qGetPtrHelper是一个函数模板重载,用于获取指针。 template <typename T> inline T *qGetPtrHelper(T *ptr) { return ptr; } template <typename Ptr> inline auto qGetPtrHelper(Ptr &ptr) -> decltype(ptr.operator->()) { return ptr.operator->(); }