Qt 的 D-Pointer

此篇文章內容讀自:https://wiki.qt.io/D-Pointer

在 Qt 原始碼中,常常看見直接引見一個 d pointer (如 d->text = text; ),這個 d pointer 是什麼?為什麼要用它?

其實從 Qt source 中可以看到,許多公開的 class,其內部都會再有一個以 Private 為結尾名的 class 來實作內容。為何?其實是為了 lib 的 binary compatibility。

假使 app1 用到了 lib1 中的某一個 class1,如果我們在 class1 加一個成員變數,app1必需重新編譯,否則執行時可能會當掉。為什麼?因為編譯時物件的大小已經是固定了。比如說我們在一函式中宣告了 class1 的 instance,那麼就會在 stack 中佔用與 class1 相等大小的空間。存取 class1 的變數事實上就是透過 offset 來做。那如果 class1 的大小改變了,app1 沒重編的話,在 stack 產生的大小就不對,在存取時就可能當掉。

那麼要如何做到 binary compatibility?答案就是使用指標,即這邊要介紹的 d pointer。

以下面例子來說,要公開的 class 為 Widget,其中存了一個指向 WidgetPrivate 指標 d_ptr。WidgetPrivate 中宣告了實際要用的成員變數,在 Widget 中要取得成員變數內則透過 d_ptr 來取得。如此當 WidgetPrivate 改變時,使用它的 app 並不需要重新編譯。(d_ptr 是透過 Widget 建構子中 new 出來,所以只要 WidgetPrivate 大小改了,new 出來的大小也會變)
widget.h
/* Since d_ptr is a pointer and is never referended 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, which is the private header file of the widget class
/* 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;
} 

在 Qt 中還有個 q pointer,它則是用來讓 WidgetPrivate 能夠存取到 Widget。q 為 d 的上下顛倒,用這樣記就比較不會忘記它的用途。

使用 d pointer最大的好處在於 binary compatibility。另外附加的好處為:

  1. 將實作隱藏。上述例子中,Widget 實作放在 WidgetPrivate 中,公開 lib 時只需要 binary 與 .h 檔即可。重點在於 WidgetPrivate 在 widget.h 中是使用指標,所以用 forward declaration 便可以編譯過了,它的宣告可以在不同檔(widget_p.h)
  2. 公開的 .h 檔可以當作 API 參考。這點很明瞭,不多說
  3. 實作放在 .cpp 中,編譯比較快
大致上 d pointer 就是如此。在文章中還有指出,因為 d_ptr 是透過 new WidgetPrivate 得到,如果有一個 Label class 繼承了 Widget,則它仍必需有個 d_ptr 為 LabelWidget。這樣子繼承愈來愈多的話,會產生很多的 d_ptr。而解決方式如下:

widget.h
class Widget {
 public:
  Widget();
 ...
 protected:
  // only sublasses 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 的建構子中初始化了 LabelPrivate 給 Widget 當作 d_ptr,所以 d_ptr 空間僅佔用在 Widget 中,大大地節省了空間。但是缺點就是,因為 d_ptr 是宣告為 WidgetPrivate *,所以如果要用到 LabelPrivate 裡頭的函式,則要透過 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 很麻煩,因此 Qt 用了一個 Q_D macro 來方便做這件事:

label.cpp
// 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_Q macro 針對 q_ptr 的使用。這兩個 macro 的宣告為:

global.h
 #define Q_D(Class) Class##Private * const d = d_func()
 #define Q_Q(Class) Class * const q = q_func()

這裡用到一個 d_func(),它要透過 Q_DECLARE_PRIVATE macro 來定義。所以在 qlabel.h 中,我們會看到這樣:

qlabel.h
class QLabel {
 private:
  Q_DECLARE_PRIVATE(QLabel);
};

所以要按照 Qt 的方式來實作公開的 API,則在公開的 class 中使用 Q_DECLARE_PRIVATE,然後再實作一個 Private class 來隱藏我們的實作並達到 binary compatibility。這樣講起來似乎很簡單?概念大概是這樣,但是要實作的時候,仍要多參考 Qt source 看看他們是如何做的。

留言

熱門文章