前言
在探索Qt源码的过程中会看到类的成员有一个d指针,d指针类型是一个private的类,这种设计模式称为PIMPL(pointer to implementation),本文根据Qt官方文章介绍d指针与q指针,理解其中的设计哲学。
PIMLP与二进制兼容性
PIMPL(pointer to implementation)或称Opaque Pointer指的是将一个类的功能实现通过另一个实现类的指针隐藏起来,例如头文件有如下声明:
class Widget {
public:
Widget();
...
private:
WidgetImpl* p;
}
在cpp中实现WidgetImpl类
class WidgetImpl {
// implementation
}
这种设计模式通常使用在库的实现当中,因为它有两个好处:
- 隐藏类实现。包括一些没必要暴露给用户的内部函数声明,以及需要的成员变量等实现细节。
- 库的二进制兼容。当改变实现类的数据成员时,不影响库的二进制兼容性,原本使用库的主程序不需要重新编译,只需要把库文件(动态库)进行替换即可。
让我们看一个二进制不兼容的例子,假设有如下实现,并且将其编译成WidgetLib1.0动态库:
class Widget
{
// ...
private:
Rect m_geometry;
};
class Label : public Widget
{
public:
// ...
String text() const
{
return m_text;
}
private:
String m_text;
}
当我们有一天希望升级Widget类的功能,需要新增一个数据成员,如下:
class Widget
{
// ...
private:
Rect m_geometry;
String m_stylesheet; // NEW in WidgetLib 1.1
};
class Label : public Widget
{
public:
// ...
String text() const
{
return m_text;
}
private:
String m_text;
};
此时编译出WidgetLib1.1库后,替换1.0库,这时运行主程序会发生崩溃。原因在于我们新增了一个数据成员,从而改变了类Widget的大小,当编译器在编译生成底层代码时,它会使用到数据成员的偏移量从而访问某个对象的某一个数据成员,下面是WidgetLib1.0和WidgetLib1.1简化后label对象的内存分布对比。
在WidgetLib1.0中,m_text在label对象偏移量为1的位置,而在WidgetLib1.1中,m_text偏移量为2。对于主程序而言,在编译使用1.0版本的主程序时,主程序中调用text()接口的代码会被翻译成访问label对象偏移量为1的位置的数据,而在升级到WidgetLib1.1后,由于主程序没有重新编译,只是替换了库,库中的m_text偏移量变为了2,但是主程序由于没有重新编译,因此它访问的仍然是偏移量为1的位置,但是此时访问到的实际上是m_stylesheet的变量。
这里text()的代码实现的翻译在主程序中而不是在lib中,是因为其实现是在头文件中写的,那么如果不是写在头文件中结果有变化吗?答案是没有,因为编译器依赖于对象的大小生成代码,并且要求编译时和运行时的对象大小是一致的,如果我们主程序中声明了一个在栈上的label对象,编译器在编译时(主程序+1.0库)认为对象的大小是2,而升级库到1.1后,主程序运行时的实际大小为3,那么在创建对象的时候就会把栈中的数据覆盖掉从而破坏栈。
至此我们得出一个结论,如果希望程序在库升级后能继续使用,我们就不能改变类的大小。
D指针
解决方法就是让导出的类拥有一个指针,这个指针指向了所有内部的数据,当内部数据的成员增减时,由于这个指针只在库中用到,因此只会影响到库,对主程序而言,类的大小一直都是一个内部数据指针的大小,因此不会对主程序产生影响,这个指针在Qt中称为D指针。
/* Since d_ptr is a pointer and is never referenced in header file
(it would cause a compile error) WidgetPrivate doesn't have to be included,
but forward-declared instead.
The definition of the class can be written in widget.cpp or
in a separate file, say widget_p.h */
class WidgetPrivate;
class Widget
{
// ...
Rect geometry() const;
// ...
private:
WidgetPrivate *d_ptr;
};
在widget_p.h中
/* widget_p.h (_p means private) */
struct WidgetPrivate
{
Rect geometry;
String stylesheet;
};
widget.cpp
// With this #include, we can access WidgetPrivate.
#include "widget_p.h"
Widget::Widget() : d_ptr(new WidgetPrivate)
{
// Creation of private data
}
Rect Widget::geometry() const
{
// The d-ptr is only accessed in the library code
return d_ptr->geometry;
}
class Label : public Widget
{
// ...
String text();
private:
// Each class maintains its own d-pointer
LabelPrivate *d_ptr;
};
label.cpp
// Unlike WidgetPrivate, the author decided LabelPrivate
// to be defined in the source file itself
struct LabelPrivate
{
String text;
};
Label::Label() : d_ptr(new LabelPrivate)
{
}
String Label::text()
{
return d_ptr->text;
}
由于d指针只在库中使用,而每次发布库都会重新编译,因此Private类可以随意更改而不会影响主程序。
这种实现方式有如下好处:
- 二进制兼容性
- 隐藏实现细节。只需一个头文件和一个库。
- 头文件没有实现细节相关的api,用户可以更清晰的看到能使用的api
- 编译更快。因为所有实现细节都从头文件都移到了实现类的cpp文件中
Q指针
有时在Private实现类中我们希望访问原有类的指针,调用它的一些函数,因此在实现类中通常会保存一个指针指向原有的类,这个指针我们称为Q指针。
widget.h
class WidgetPrivate;
class Widget
{
// ...
Rect geometry() const;
// ...
private:
WidgetPrivate *d_ptr;
};
widget_p.h
struct WidgetPrivate
{
// Constructor that initializes the q-ptr
WidgetPrivate(Widget *q) : q_ptr(q) { }
Widget *q_ptr; // q-ptr points to the API class
Rect geometry;
String stylesheet;
};
widget.cpp
#include "widget_p.h"
// Create private data.
// Pass the 'this' pointer to initialize the q-ptr
Widget::Widget() : d_ptr(new WidgetPrivate(this))
{
}
Rect Widget::geometry() const
{
// the d-ptr is only accessed in the library code
return d_ptr->geometry;
}
label.h
class Label : public Widget
{
// ...
String text() const;
private:
LabelPrivate *d_ptr;
};
label.cpp
// Unlike WidgetPrivate, the author decided LabelPrivate
// to be defined in the source file itself
struct LabelPrivate
{
LabelPrivate(Label *q) : q_ptr(q) { }
Label *q_ptr;
String text;
};
Label::Label() : d_ptr(new LabelPrivate(this))
{
}
String Label::text()
{
return d_ptr->text;
}
优化d指针继承
注意到Widget和Label类都声明了一个各自类的Private类指针,且子类构造函数在实例化父类时使用的是默认无参的构造函数,因此,当我们实例化Label时,会先调用基类的构造函数,然后new WidgetPrivate,接着调用子类的构造函数,然后new LabelPrivate,对于某些深度继承的类,这种设计将会造成多次的内存申请,且有多个相互独立的Private类对象存在,子类的d_ptr还会覆盖父类的同名数据成员d_ptr。解决方法是让Private类也具有继承关系,将子类的指针沿着Private类继承链向上传递。注意这种方式要求Private父类要在单独的头文件中声明(而不是直接写在cpp文件中),否则无法被其他Private子类继承。改进后的设计如下:
widget.h
class Widget
{
public:
Widget();
// ...
protected:
// only subclasses may access the below
// allow subclasses to initialize with their own concrete Private
Widget(WidgetPrivate &d);
WidgetPrivate *d_ptr;
};
widget_p.h
struct WidgetPrivate
{
WidgetPrivate(Widget *q) : q_ptr(q) { } // constructor that initializes the q-ptr
Widget *q_ptr; // q-ptr that points to the API class
Rect geometry;
String stylesheet;
};
widget.cpp
Widget::Widget() : d_ptr(new WidgetPrivate(this))
{
}
Widget::Widget(WidgetPrivate &d) : d_ptr(&d)
{
}
label.h
class Label : public Widget
{
public:
Label();
// ...
protected:
Label(LabelPrivate &d); // allow Label subclasses to pass on their Private
// notice how Label does not have a d_ptr! It just uses Widget's d_ptr.
};
label.cpp
#include "widget_p.h"
class LabelPrivate : public WidgetPrivate
{
public:
String text;
};
Label::Label()
: Widget(*new LabelPrivate) // initialize the d-pointer with our own Private
{
}
Label::Label(LabelPrivate &d) : Widget(d)
{
}
当我们创建Label对象,只会发生一次内存申请,即new LabelPrivate。
Q_D和Q_Q
在Label类方法中,当我们访问d_ptr时,访问的是基类声明的WidgetPrivate类型的指针,在Label类的方法中为了访问LabelPrivate的成员text,需要向下转换
void Label::setText(const String &text)
{
LabelPrivate *d = static_cast<LabelPrivate*>(d_ptr); // cast to our private type
d->text = text;
}
Qt定义了Q_D函数帮我们做上述的转化,以简化代码,Q_Q函数类似:
#define Q_D(Class) Class##Private * const d = d_func()
#define Q_Q(Class) Class * const q = q_func()
其中d_func()是把d_ptr进行类似static_cast<LabelPrivate*>的类型转换。于是代码简化为:
// 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();
}
d_func()通过Q_DECLARE_PRIVATE定义了两个版本,一个返回Private类指针,一个返回const Private类指针,此外Q_DECLARE_PRIVATE还声明ClassPrivate是Class的友元类,如下:
#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;
我们可以在公共类(导出类)声明一个Q_DECLARE_PRIVATE,从而快速声明返回const和非const Private类指针的d_func(),以及声明Private类是当前类的友元类,如下:
class QLabel
{
private:
Q_DECLARE_PRIVATE(QLabel)
};
friend class Class##Private
的作用是让Private类可以访问公共类的public/protected/private接口。
注意,当我们需要使用const版本的Private类指针时,使用如下写法:
Q_D(const Label); // 自动调用const版本的d_func()
通常d_func()是在类内部使用,不过某些情况下,也可以通过声明friend class的方式使得其它类可以访问当前类内部的数据,这些数据通常无法从当前类的公共api得到,例如,在QLabel类中声明ClassA是友元类,ClassA对象访问QLabel内部数据的方法如下:
// ClassA声明为QLabel的friend class, ClassA方法中可以有如下调用
label->d_func()->linkClickCount;
参考:
- https://wiki.qt.io/D-Pointer