Qt d-pointer & q-pointer

每次看 Qt source code 都不知道 d->q-> 到底在幹嘛,翻到 D-Pointer 這篇解釋就來看一下。但是我原本不是要追究這個的ㄚㄚㄚ……

d-pointer pattern

d-pointer 是個 design pattern,也叫 Opaque pointer 或 Pimpl idiom。它分開給外面看的公開 class interface 跟實作的 class。做法是在公開 class 放個實作 class 的 pointer,公開 class 各 function 只是 call implementation class 的 function。公開 class 中跟實作相關的 member 只有那個 pointer,所以 size 會固定是 pointer 的 size。老覺得在哪看過類似的東西……

Qt 用 d-pointer 主要是為了保持 binary compatibility。

Binary Compatibility

有 binary compatibility 表示 application 在以舊版 library compile 後,新版 library binary 可以直接取代舊版 library 而不需要重新 compile application。要達到 binary compatibility 就不能改變已經公開出去的 C++ class 的 size 或 memory layout,即不能增加 member 或者更改 function 跟 member 的順序。如果改了,application 認定的 library class size 或 memory layout 跟新版 library 不同,會把 memory 踩爛掉。

Qt 使用 d-pointer 達到不修改既有公開 class 還能增加 feature 或修改實作細節。

Qt 的實作

基於保持 binary compatibility,Qt 幾乎所有 class 都用 d-pointer 指向真正實作的 class。實作 class 叫 QXXXPrivate,例如 QWidget 就有 QWidgetPrivate,通常定義在 xxx_p.h,例如 qwidget_p.hQXXX 以及 QXXXPrivate 的實作會寫在 qxxx.cpp

為了減少 memory allocation 的次數,QXXXPrivate 的 class 也會有繼承關係。其繼承關係會依循一般 class 的繼承關係,例如 QLabel 繼承 QFrameQLabelPrivate 就會繼承 QFramePrivateQXXXPrivate 大多繼承自 QObjectPrivate

以下 code 擷取自 Qt 5.7.0。

d-pointer & q-pointer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Q_CORE_EXPORT QObject
{
...
protected:
QScopedPointer<QObjectData> d_ptr;
...
};

class Q_CORE_EXPORT QObjectData
{
public:
QObject *q_ptr;
...
};

class Q_CORE_EXPORT QObjectPrivate : public QObjectData { ... };

好啦~上面是 d-pointer,QObjectPrivate 繼承 QObjectData,而 QObjectd_ptr 指向 QObjectData

有時候實作 class 需要使用公開 class,因此實作 class 會存公開 class 的 pointer,稱為 q-pointer。如上,QObjectDataQObject 的 pointer q_ptr

在 Qt 裡 d-pointer 跟 q-pointer 基本上是繼承自 base class,不過也有例外。

Q_D() & Q_Q () macro

qtbase/src/corelib/global/qglobal.h 定義了這些 function 跟 macro:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
template <typename T> static inline T *qGetPtrHelper(T *ptr) { return ptr; }

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

#define Q_DECLARE_PUBLIC(Class) \
inline Class* q_func() { return static_cast<Class *>(q_ptr); } \
inline const Class* q_func() const { return static_cast<const Class *>(q_ptr); } \
friend class Class;

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

在 class 內會用 Q_DECLARE_PRIVATE(),以 QStandardItem 為例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Q_GUI_EXPORT QStandardItem
{
...
protected:
QScopedPointer<QStandardItemPrivate> d_ptr;

private:
Q_DECLARE_PRIVATE(QStandardItem)
...
};

QStandardItem::QStandardItem() : d_ptr(new QStandardItemPrivate)
{
Q_D(QStandardItem);
d->q_ptr = this;
}

void QStandardItem::setChild(int row, int column, QStandardItem *item)
{
Q_D(QStandardItem);
d->setChild(row, column, item, true);
}

QStandardItem 裡有兩個 inline 的 private function 分別為 QStandardItemPrivate* d_func() 以及 const QStandardItemPrivate* d_fun() const,並且 QStandardItemPrivateQStandardItem 的 friend class,可以 access QStandardItem 的 private member。constructor 生了 QStandardItemPrivate 並將 d_ptr 指過去。

依據 macro 的定義,dQStandardItemPrivate*d_func()qGetPtrHelper(d_ptr) 轉型成 QStandardItemPrivate* 丟出來。d_ptr 是個 QScopedPointer<QStandardItemPrivate>qGetPtrHelper() 將 smart pointer 轉成單純的 pointer(參考這篇)。還沒有看得很懂怎麼轉的,不過先這樣理解吧… 總之 d 是指向 construct 時塞進來的 QStandardItemPrivate 的 pointer。所以最後 QStandardItem::setChild() 是用 QStandardItemPrivate::setChild() 實際做事。

反過來的 Q_Q() 比較簡單。這 macro 的名字真好笑(離題

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class QStandardItemPrivate
{
Q_DECLARE_PUBLIC(QStandardItem)
public:
...
QStandardItem *q_ptr;
};

void QStandardItemPrivate::setChild(int row, int column, QStandardItem *item,
bool emitChanged)
{
Q_Q(QStandardItem);
...
if (rows <= row)
q->setRowCount(row + 1);
...
}

透過 q_func() 拿到的 q 是轉型成 QStandardItem* 的 pointer。

這個例子裡,因為 QStandardItemQStandardItemPrivate 沒有繼承別人,它們自己分別記錄了 d_ptrq_ptr,所以 d_func()q_func() 有沒有轉型根本沒差。但其他繼承 QObjectPrivate 的 class,q_ptr 是從 QObjectData 來的,type 是 QObject*,如果要直接使用 sub class 的 member 就得轉型,d_ptr 同理。除非用 virtual function 做多型,不過這個狀況用 virtual function 很怪,因為這樣 base class QObject 得有所有 sub class 會用到的 interface,這邊顯然不是這個意思,用 virtual function 不合理。

這樣看過之後,對 Qt source code 的結構比較理解了,才不會每次看到 Q_D()Q_Q() 啊都想 Q_Q 了。