Qt-D指针与Q指针的设计哲学

发布于:2025-03-17 ⋅ 阅读:(19) ⋅ 点赞:(0)

前言

在探索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
}

这种设计模式通常使用在库的实现当中,因为它有两个好处:

  1. 隐藏类实现。包括一些没必要暴露给用户的内部函数声明,以及需要的成员变量等实现细节。
  2. 库的二进制兼容。当改变实现类的数据成员时,不影响库的二进制兼容性,原本使用库的主程序不需要重新编译,只需要把库文件(动态库)进行替换即可。

让我们看一个二进制不兼容的例子,假设有如下实现,并且将其编译成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类可以随意更改而不会影响主程序。

这种实现方式有如下好处:

  1. 二进制兼容性
  2. 隐藏实现细节。只需一个头文件和一个库。
  3. 头文件没有实现细节相关的api,用户可以更清晰的看到能使用的api
  4. 编译更快。因为所有实现细节都从头文件都移到了实现类的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;

参考:

  1. https://wiki.qt.io/D-Pointer