什么是d-pointer

如果你曾经查看过Qt的源代码文件,比如这个,你会发现其中有很多宏 Q_DQ_Q 的用法。本文就来揭开这些宏使用的目的。

Q_DQ_Q 宏定义是 d-pointer(也被称为 opaque pointer 设计模式的一部分), 它可以把一个类库的实施细节对使用的用户隐藏, 而且对实现的更改不会打破二进制的兼容性。

什么是二进制兼容

在设计像 Qt 这样的类库的时候,理想的行为应该是动态连接到 Qt 的应用程序,甚至在将 Qt 类库升级或者替换到另外一个版本的时候,不需要重新编译就可以继续运行。例如,你的应用程序 CuteApp 是基于 Qt 4.5 的,你应该可以把你的 Qt 由4.5升级到 Qt 4.6 (在 Windows 下用安装程序,在 Linux 下通常有包管理器自动完成),而你用 Qt 4.5 构建的 CuteApp 应该还可以运行。

什么会打破二进制兼容

那么,什么时候类库的变化需要应用程序的重新编译呢?我们来看一个简单的例子:

class Widget 
{
    \\ …
private:
    Rect m_geometry;
 };

class Label : public Widget 
{
public:
    \\…
    String text() const { return m_text; }
private:
    String m_text;
};

这里,我们有一个 Widget,包含一个 geometry 作为成员变量。我们编译 Widget 并且发布为 WidgetLib 1.0 。对于 WidgetLib 1.1 ,有人有了新的主意,要添加样式表的支持。没问题,我们只需要添加一个新的方法和一个新的数据成员。

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

我们将带有上述更改的 WidgetLib 1.1 集成到在 WidgetLib 1. 0环境下编译并正常运行的CuteApp中,将会发现程序光荣地崩溃了!

为什么会崩溃

究其原因,是由于添加了一个新的数据成员,我们最终改变了 Widget 和 Label 对象的大小。为什么会这样?因为当你的 C++ 编译器生成代码的时候,它会用偏移量来访问对象的数据。

下面是一个 POD对象 在内存里面布局的一个简化版本。

Label 对象在 WidgetLib 1.0 的布局 Label 对象在 WidgetLib 1.1 的布局
m_geometry <偏移量 0> m_geometry <偏移量 0>
N.A m_stylesheet <偏移量 1>
m_text <偏移量 1> N.A
N.A m_text <偏移量 2>

WidgetLib 1.0 中,Label 的 text 成员在(逻辑)偏移量为1的位置。在编译器生成的代码里,应用程序的方法 Label::text() 被翻译成访问 Label 对象里面偏移量为1的位置。

WidgetLib 1.1 中,Label 的 text 成员的(逻辑)偏移量被转移到了2的位置!由于应用程序没有重新编译,它仍然认为 text 在偏移量1的位置,结果却访问了 stylesheet 变量!

我确信这个时候会有人问,为什么Label::text()的偏移量的计算的代码会在CuteApp二进制文件结束,而不是在WidgetLib的二进制文件。 答案是因为Label::text() 的代码定义在头文件里面,最终被 内联

那么,如果 Label::text() 没有定义为内联函数,情况会改变吗?这么讲,Label::text() 被移到源文件里面?嗯,不会,程序仍然会崩溃。C++编译器依赖对象大小在编译时和运行时相同。比如,堆栈的 winding/unwinding - 如果你在堆栈上创建了一个 Label 对象, 编译器产生的代码会根据 Label 对象在编译时的大小在堆栈上分配空间。由于Label的大小在 WidgetLib 1.1 运行时已经不同,Label 的构造函数会覆盖已经存在的堆栈数据,最终破坏堆栈。

不要改变导出的 C++ 类的大小

总之,一旦你的类库发布了,永远不要改变导出的 C++ 类的大小或者布局(不要移动成员)。C++ 编译器生成的代码会假定,一个类的大小和成员的顺序 ''编译后'' 就不会改变.

那么,如何在不改变对象的大小的同时添加新的功能呢?

d-pointer

诀窍是通过保存唯一的一个指针而保持一个类库所有公共类的大小不变。这个指针指向一个包含所有数据的私有的(内部的)数据结构。内部结构的大小可以增大或者减小,而不会对应用程序带来副作用,因为指针只会被类库里面的代码访问,从应用程序的视角来看,对象的大小并没有改变 - 它永远是指针的大小。 这个指针被叫做 d-pointer

这个模式的精髓可以由下面的代码来概述(本文中的所有代码都没有析构函数,在实际使用的时候应该加上它)。

/* 
    \file widget.h

    由于 d_ptr 是一个指针,并且永远不会在头文件引用它(如果引用,会产生编译错误),
    WidgetPrivate 没有必要在头文件中包含,只需要前置声明。
    WidgetPrivate 的定义可以在 widget.cpp 或者一个单独的文件(比如 widget_p.h)
    中实现。
*/

class WidgetPrivate;

class Widget
{
    // ...
    Rect geometry() const;
    // ... 

private:
    WidgetPrivate* d_ptr;
};

/* 
    \file widget_p.h 

    _p表示私有的
*/
struct WidgetPrivate
{
    Rect geometry;
    String stylesheet;
};

/*
    \file widget.cpp 

    包含头文件 widget_p.h,我们就可以使用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;
}

/*
    \file label.h

    Widget 子类中的用法
*/

class Label : public Widget
{
    // ...
    String text();

private:
    // Each class maintains its own d-pointer
    LabelPrivate* d_ptr;
};

/*
    \file label.cpp

    不同与 WidgetPrivate, LabelPrivate在 label.cpp 中定义
*/

struct LabelPrivate
{
    String text;
};

Label::Label() : d_ptr(new LabelPrivate)
{
}

String Label::text()
{
    return d_ptr->text;
}

有了上面的结构,CuteApp 从来不需要直接访问 d-pointer。由于 d-pointer 只是在 WidgetLib 被访问,而 WidgetLib 在每一次发布都被重新编译,私有的类可以随意的改变而不会对 CuteApp 带来影响。

d-pointer 的其它好处

这里不全都是和二进制兼容有关。d-pointer 还有其它的好处:

  • 隐藏了实现细节 - 我们可以只发布带有头文件和二进制文件的 WidgetLib。源文件可以是闭源代码的。
  • 头文件很干净,不包含实现细节,可以直接作为 API 参考。
  • 由于实现需要的包含的头文件从头文件里已到了实现(源文件)里面,编译速更快。(译:降低了编译依赖)

事实上,上边的好处是微乎其微的。Qt 使用 d-pointer 的真正原因是为了二进制兼容和 Qt 最初是封闭源代码的.(译:Qt 好像没有封闭源代码)

q-pointer

到目前为止,我们仅仅看到的是作为 C 风格的数据机构的 d-pointer。实际上,它可以包含私有的方法(辅助函数)。例如,LabelPrivate 可以有一个getLinkTargetFromPoint() 辅助函数,当鼠标点击的时候找到目标链接。在很多情况下,这些辅助函数需要访问公有类,也就是 Label 或者它的父类 Widget 的一些函数。比如,一个辅助函数 setTextAndUpdateWidget() 想要调用一个安排重画Widget的公有方法 Widget::update()。所以,WidgetPrivate 存储了一个指向公有类的指针,称为q-pointer。修改上边的代码引入q-pointer,我们得到下面代码:

/* 
    \file widget.h

    由于 d_ptr 是一个指针,并且永远不会在头文件引用它(如果引用,会产生编译错误),
    WidgetPrivate 没有必要在头文件中包含,只需要前置声明。
    WidgetPrivate 的定义可以在 widget.cpp 或者一个单独的文件(比如 widget_p.h)
    中实现。
*/

class WidgetPrivate;

class Widget
{
    // ...
    Rect geometry() const;
    // ... 

private:
    WidgetPrivate* d_ptr;
};

/* 
    \file widget_p.h 

    _p表示私有的
*/
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;
};

/*
    \file widget.cpp 

    包含头文件 widget_p.h,我们就可以使用WidgetPrivate类了,
    构造 WidgetPrivate 时,传入 this 指针初始话 q-ptr
*/
#include "widget_p.h"

Widget::Widget() : d_ptr(new WidgetPrivate(this))
{
    // Creation of private data
}

Rect Widget::geometry() const
{
    // The d-ptr is only accessed in the library code
    return d_ptr->geometry;
}

/*
    \file label.h

    Widget 子类中的用法
*/

class Label : public Widget
{
    // ...
    String text();

private:
    // Each class maintains its own d-pointer
    LabelPrivate* d_ptr;
};

/*
    \file label.cpp

    不同与 WidgetPrivate, LabelPrivate在 label.cpp 中定义
*/

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

使用继承进一步优化

对于上边的代码,创建一个 Label 会带来 LabelPrivateWidgetPrivate 的内存分配。如果我们在Qt里面采用这种策略,对于一些像 QListWidget 的类,情况会相当糟糕 - 它有6层的继承层次,也就会带来最多6次的内存分配。

通过对我们的私有类添加一个继承层次,解决了这个问题,这样类实例化时将一个 d-pointer 层层向上传递。

/* 
    \file widget.h

    由于 d_ptr 是一个指针,并且永远不会在头文件引用它(如果引用,会产生编译错误),
    WidgetPrivate 没有必要在头文件中包含,只需要前置声明。
    WidgetPrivate 的定义可以在 widget.cpp 或者一个单独的文件(比如 widget_p.h)
    中实现。
*/

class WidgetPrivate;

class Widget
{
    // ...
    Rect geometry() const;
    // ... 

protected:
    Widget(WidgetPrivate &d);
    WidgetPrivate* d_ptr;
};

/* 
    \file widget_p.h 

    _p表示私有的
*/
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;
};

/*
    \file widget.cpp 

    包含头文件 widget_p.h,我们就可以使用WidgetPrivate类了,
    构造 WidgetPrivate 时,传入 this 指针初始话 q-ptr
*/
#include "widget_p.h"

Widget::Widget() : d_ptr(new WidgetPrivate(this))
{
    // Creation of private data
}

Widget::Widget(WidgetPrivate &d) : d_ptr(&d)
{

}

Rect Widget::geometry() const
{
    // The d-ptr is only accessed in the library code
    return d_ptr->geometry;
}

/*
    \file label.h

    Widget 子类中的用法
*/

class Label : public Widget
{
    // ...
    String text();

protected:
    Label(LabelPrivate &d);
};

/*
    \file label.cpp

    不同与 WidgetPrivate, LabelPrivate在 label.cpp 中定义
*/

struct LabelPrivate : public WidgetPrivate
{
    String text;
};

Label::Label() : d_ptr(new LabelPrivate(this))
{
}

Label::Label(LabelPrivate &d) : Widget(d)
{

}

String Label::text()
{
    return d_ptr->text;
}

是不是很漂亮?现在当我们创建一个 Label 对象时,它会创建一个 LabelPrivate(它继承了 WidgetPrivate)。它把一个 ''d-pointer'' 实体传递给Widget的保护的构造函数。Label 也有这样一个保护的构造函数,可以被继承 Label 的类提供自己的私有类来使用。

Qt 中的 d-pointers

在 Qt 中,几乎每个公共类都是用 d-pointer 方法。唯一不使用它的情况是,当事先知道该类永远不会添加额外的成员变量时。类如QPoint, QRect等类。

注意,在 Qt 中,所有 Private 对象的基类都是 QObjectPrivate。

Q_DQ_Q

上一步优化的一个副作用是 q-ptr 和 d-ptr 的类型分别是 WidgetWidgetPrivate
这就意味着下面的代码不能工作。

 void Label::setText(const String &text) 
 {
    // 不工作。虽然 d_ptr 指向 LabelPrivate 对象,但是它是 WidgetPrivate 类型
    d_ptr->text = text;
 }

因此,在子类里访问 d-pointer 的时候,需要用 static_cast 转型到合适的类型。

 void Label::setText(const String &text) 
 {
    LabelPrivate *d = static_cast<LabelPrivate *>(d_ptr); // cast to our private type
    d->text = text;
 }

代码里到处都是 static_cast 看起来不是那么漂亮,所以我们定义了下面的宏,

 // global.h (macros)
 #define DPTR (Class) Class##Private *d = static_cast<Class##Private*>(d_ptr)
 #define QPTR (Class) Class *q = static_cast<Class*>(q_ptr)

// label.cpp
 void Label::setText(const String &text) 
 {
    DPTR (Label);
    d->text = text;
 }

void LabelPrivate::someHelperFunction() 
{
    QPTR (label);
    q->selectAll(); // we can call functions in Label now
 }

Q_DECLARE_PRIVATEQ_DECLARE_PUBLIC

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

可以这么用:

class QLabel 
{
private:
    Q_DECLARE_PRIVATE(QLabel);
};

这里的想法是 QLabel 提供了一个函数 d_func() 允许访问它的私有内部类。这个方法本身是私有的(因为这个宏定义在 qlabel.h 私有区域)。 但是 d_func() 可以被 QLabel友元函数 (C++ 友元)调用。这主要对一些 Qt 类想获得 QLabel 的无法通过公有 API 访问的一些信息有用。例如,QLabel 可能要跟踪用户点击了一个链接多少次。但是没有公有 API 访问这个信息。QStatistics 是需要这个信息的一个类。Qt开发人员可以添加 QStatistics 作为 QLabel 的一个友元类,这样QStatistics 就可以 label->d_func()->linkClickCount 来访问。

d_func 还有一个优点是保证了const正确性:在 MyClass 的一个 const 成员函数里,你需要 Q_D(const MyClass),因此你只能调用 MyClassPrivate 的const成员函数。如果用 "naked" 的 d_ptr 你可以调用非const函数。

posted @ 2020-07-05 14:43  hkeeplearning  阅读(597)  评论(0)    收藏  举报