简体中文 English Български Español

什么是d-pointer

如果你曾经查看过Qt的源代码文件,例如 这个 [qt.gitorious.com], 你会发现里面有很多的 Q_DQ_Q 宏定义. 本文就来揭开这些宏使用的目的。

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

什么是二进制兼容?

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

什么会打破二进制兼容

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

  1.     class Widget {
  2.            ...
  3.     private:
  4.         Rect m_geometry;
  5.     };
  6.  
  7.     class Label : public Widget {
  8.             ...
  9.         String text() const { return m_text; }
  10.     private:
  11.         String m_text;
  12.     };

这里,我们有一个Widget, 包含一个geometry作为成员变量。 我们编译Widget并且发布它为*WidgetLib 1.0*。

对于 *WidgetLib 1.1*, 有人有了新的主意要添加样式表支持。没问题,我们只需要添加一个新的方法和一个新的 _数据成员_。

  1.     class Widget {
  2.          ...
  3.     private:
  4.          Rect m_geometry;
  5.          String m_stylesheet; // NEW in WidgetLib 1.1
  6.     };
  7.  
  8.     class Label : public Widget {
  9.     public:
  10.         ...
  11.         String text() const { return m_text; }
  12.     private:
  13.         String m_text;
  14.     };

我们发布WidgetLib 1.1,仅仅包含上面的变化,却发现和WidgetLib 1.0一起编译并且运行正常的 CuteApp光荣地崩溃了!

为什么会崩溃?

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

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

Label对象在WidgetLib 1.0的布局 Label对象在WidgetLib 1.0的布局
m_geometry <偏移量 0> m_geometry <偏移量 0>
——————— m_stylesheet <偏移量 1>
m_text <偏移量 1> ————————-  
———————- m_text <偏移量 2>

在 !WidgetLib 1.0, Lable的text成员在(逻辑)偏移量为1的位置。在编译器生成的代码里,应用程序的方法Label::text()被翻译成访问Lable对象里面偏移量为1的位置。
在In WidgetLib 1.1, Label的’‘text’‘成员的(逻辑)偏移量被转移到了2的位置! 由于应用程序没有重新编译,它仍然认为text在偏移量1的位置,结果却访问了
stylesheet变量!

对刚刚入门C++的新兵 :-)

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

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

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

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

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

d-pointer

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

这个模式的精神可以由下面的代码来概述。

  1.     /* widget.h */
  2.     // 前置声明. 定义在 widget.cpp或者
  3.     // 单独的一个文件,比如widget_p.h
  4.     class WidgetPrivate;
  5.  
  6.     class Widget {
  7.        ...
  8.        Rect geometry() const;
  9.        ...
  10.     private:
  11.        // d-pointer never referenced in header file.
  12.        // Since WidgetPrivate is not defined in this header,
  13.        // any access will be a compile error
  14.        WidgetPrivate *d_ptr;
  15.     };
  16.  
  17.     /* widget_p.h */ (_p 意味着私有)
  18.     struct WidgetPrivate {
  19.         Rect geometry;
  20.         String stylesheet;
  21.     };
  22.  
  23.     /* widget.cpp */
  24.     #include "widget_p.h"
  25.     Widget::Widget()
  26.         : d_ptr(new WidgetPrivate) // 创建私有数据{
  27.     }
  28.  
  29.     Rect Widget::geoemtry() const {
  30.         //  d-ptr 仅仅被类库代码访问
  31.         return d_ptr->geometry;
  32.     }
  33.  
  34.     /* label.h */
  35.     class Label : public Widget {
  36.        ...
  37.        String text();
  38.     private:
  39.        // 每个类维护自己的d-pointer
  40.        LabelPrivate *d_ptr;
  41.     };
  42.  
  43.     /* label.cpp */
  44.     // 和WidgetPrivate不同, 我们在它自己的源文件里定义LabelPrivate
  45.     struct LabelPrivate {
  46.         String text;
  47.     };
  48.    
  49.     Label::Label()
  50.         : d_ptr(new LabelPrivate) {
  51.     }
  52.  
  53.     String Label::text() {
  54.         return d_ptr->text;
  55.     }

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

d-pointer的其它好处

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

  • 隐藏了实现细节 – 我们可以只发布带有头文件和二进制文件的WidgetLib。源文件可以是闭源代码的。
  • 头文件很干净,不包含实现细节,可以直接作为API参考。The header file is clean of implementation details and can serve as the API reference.
  • 由于实施需要的包含的头文件从头文件里已到了实施(源文件)里面,编译速更快。(译:降低了编译依赖)

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

The q-pointer

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

  1.     /* widget.h */
  2.     // 前置声明. 定义在 widget.cpp
  3.     // 或者单独的一个文件,比如widget_p.h
  4.     class WidgetPrivate;
  5.  
  6.     class Widget {
  7.        ...
  8.        Rect geometry() const;
  9.        ...
  10.     private:
  11.        // d-pointer 从来不在头文件被引用
  12.        // 因为WidgetPrivate没有在头文件里定义,
  13.        // 所有对它的访问都会带来编译错误
  14.        WidgetPrivate *d_ptr;
  15.     };
  16.  
  17.     /* widget_p.h */ (_p意味着私有)
  18.     struct WidgetPrivate {
  19.         // constructor that initializes the q-ptr
  20.         WidgetPrivate(Widget *q) : q_ptr(q) { }
  21.         Widget *q_ptr; // q-ptr that points to the API class
  22.         Rect geometry;
  23.         String stylesheet;
  24.     };
  25.  
  26.     /* widget.cpp */
  27.     #include "widget_p.h"
  28.     // create private data. pass the 'this' pointer to initialize the q-ptr
  29.     Widget::Widget()
  30.         : d_ptr(new WidgetPrivate(this)) {
  31.     }
  32.  
  33.     Rect Widget::geoemtry() const {
  34.         // the d-ptr is only accessed in the library code
  35.         return d_ptr->geometry;
  36.     }
  37.  
  38.     /* label.h */
  39.     class Label : public Widget {
  40.        ...
  41.        String text() const;
  42.     private:
  43.        LabelPrivate *d_ptr; // each class maitains it's own d-pointer
  44.     };
  45.  
  46.     /* label.cpp */
  47.     // Unlike WidgetPrivate, we define LabelPrivate in the source file
  48.     struct LabelPrivate {
  49.         LabelPrivate(Label *q) : q_ptr(q) { }
  50.         Label *q_ptr;
  51.         String text;
  52.     };
  53.    
  54.     Label::Label()
  55.         : d_ptr(new LabelPrivate(this)) {
  56.     }
  57.  
  58.     String Label::text() {
  59.         return d_ptr->text;
  60.     }

进一步优化

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

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

  1.    /* widget.h */
  2.    class Widget {
  3.    public:
  4.       Widget();
  5.        ...
  6.    protected:
  7.       // only sublasses may access the below
  8.       Widget(WidgetPrivate &d); //允许子类通过他们自己的实体私有对象来初始化
  9.       WidgetPrivate *d_ptr;
  10.    };
  11.  
  12.     /* widget_p.h */ (_p means private)
  13.     struct WidgetPrivate {
  14.         WidgetPrivate(Widget *q) : q_ptr(q) { } // 构造函数初始化q-ptr
  15.         Widget *q_ptr; // 指向API类的
  16.         Rect geometry;
  17.         String stylesheet;
  18.     };
  19.  
  20.    /* widget.cpp */
  21.    Widget::Widget()
  22.      : d_ptr(new WidgetPrivate(this)) {
  23.    }
  24.    
  25.    Widget::Widget(WidgetPrivate &d)
  26.      : d_ptr(&d) {
  27.    }
  28.  
  29.    /* label.h */
  30.    class Label : public Widget {
  31.    public:
  32.       Label();
  33.        ...
  34.    protected:
  35.       Label(LabelPrivate &d); // 允许Label的子类传递自己的私有数据
  36.       //注意Label没有d_ptr!它用了父类Widgetde d_ptr.
  37.    };
  38.  
  39.    /* label.cpp */
  40.    #include "widget_p.h" // so we can access WidgetPrivate
  41.  
  42.    class LabelPrivate : public WidgetPrivate {
  43.    public:
  44.        String text;
  45.    };
  46.  
  47.    Label::Label()
  48.       : Widget(*new LabelPrivate) // 用我们自己的私有对象来初始化 d-pointer {
  49.    }
  50.    
  51.    Label::Label(LabelPrivate &d)
  52.       : Widget(d) {
  53.    }

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

把q-ptr和d-ptr转型到正确的类型(QPTR和DPTR)

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

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

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

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

代码里到处都是static_cast看起来不是那么漂亮,所以我们定义了下面的宏,
  1.     // global.h (macros)
  2.     #define DPTR(Class) Class##Private *d = static_cast<Class##Private *>(d_ptr)
  3.     #define QPTR(Class) Class *q = static_cast<Class *>(q_ptr)
  4.  
  5.     // label.cpp
  6.     void Label::setText(const String &text) {
  7.         DPTR(Label);
  8.         d->text = text;
  9.     }
  10.  
  11.     void LabelPrivate::someHelperFunction() {
  12.         QPTR(label);
  13.         q->selectAll(); // we can call functions in Label now
  14.     }

Qt中的d-pointers

在Qt中,几乎所有的公有类都使用了d-pointer。唯一不用的情况是如果事先知道某个类永远不会添加额外的成员变量。例如,像QPoint, QRect这些类,我们不期望有新的成员添加,因此它们的数据成员直接保存在类里而没用d-pointer。

  • 在Qt中,所有私有对象的基类是 QObjectPrivate
  • Q_DQ_Q 宏提供了上边讨论的QPTR 和 DPTR的功能.
  • Qt的公有类有一个 Q_DECLARE_PRIVATE的宏。这个宏的代码:
    1.     // qlabel.h
    2.     class QLabel {
    3.     private:
    4.         Q_DECLARE_PRIVATE(QLabel);
    5.     };
    6.  
    7.     // qglobal.h
    8.     #define Q_DECLARE_PRIVATE(Class) \
    9.         inline Class##Private* d_func() { return reinterpret_cast<Class##Private *>(qGetPtrHelper(d_ptr)); } \
    10.         inline const Class##Private* d_func() const { \
    11.              return reinterpret_cast<const Class##Private *>(qGetPtrHelper(d_ptr)); } \
    12.         friend class Class##Private;
这里的想法是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函数。

还有一个Q_DECLARE_PUBLIC宏做相反的事情。

Categories:

  • QtInternals
  •