天天看點

QT對象類型QObject源碼中的間接的設計思想

這一篇文章介紹QT架構中QT對象類型也就是QObject類型的源代碼在設計上的一個比較優秀的設計思想。

1. QObject類型定義

QObject

直接來看QObject的源代碼。為了表達更簡潔更直覺,這裡省略了跟本文無關的各種代碼。

QT對象類型QObject源碼中的間接的設計思想

如果檢視QObject類型定義,可以發現類型裡面有很多成員函數,但是除了 d_ptr之外,就沒有定義更多的成員變量。當然,沒有定義更多成員變量并不等于QObject對象執行個體中就隻有d_ptr這一個資料,實際上由于QObject中定義了虛函數,是以QObject對象執行個體中還有vptr,也就是指向虛函數表的指針。

現在讓我們來讨論這麼一個問題,QObject肯定是有内部狀态資料的,那麼内部狀态資料儲存在哪兒呢?實際上就是儲存在d_ptr指向的QT對象資料對象執行個體中。d_ptr是QT中的一個範圍指針,也就是說當QObject對象被銷毀時,使得d_ptr指向的QT對象資料對象執行個體也會被銷毀掉。

QObjectData

先來看QObjectData類型定義。

QT對象類型QObject源碼中的間接的設計思想

這個類型裡面定義了一個QObject指針類型的變量q_ptr。在QT對象資料類型有關的各種成員函數中,如果想通路所屬于的QObject對象的this指針或者成員函數,可以通過q_ptr這個指針來通路。

也就是說QObject和QObjectData之間互相持有了對方的對象執行個體的指針,實作了QT對象和QT對象資料的之間的雙向通路。

QObjectPrivate

這個類型是QObjectData類型的派生類型。定義了QT對象的一些私有資料。

QT對象類型QObject源碼中的間接的設計思想

2. QT對象的實際對象資料到底是什麼

QObject對象

先來看一下QObject類型對象執行個體中的對象資料是什麼。

QT對象類型QObject源碼中的間接的設計思想

如果跟蹤調試一下,可以看到QObject類型構造函數的源代碼。

QT對象類型QObject源碼中的間接的設計思想

可以看到在構造函數中一開始就實作了QObject和QObjectPrivate對象執行個體的雙向引用,當然嚴格來講是雙向的指針指向。

現在新的問題來了,QT架構中有很多具體的QT對象類型,雖然它們都是QObject的直接或間接派生類型,但是各自必然都具有各自類型的獨特的私有資料。那麼QT架構是如何實作這些類型的對象執行個體在構造時使用自己類型獨特的私有資料的呢?

QThread是一個典型正面例證

直接看QThread類型定義:

QT對象類型QObject源碼中的間接的設計思想

然後看一下QThread構造函數:

QT對象類型QObject源碼中的間接的設計思想

QThread在構造對象執行個體時先構造一個QThreadPrivate作為自己的私有資料對象執行個體,然後讓d_ptr指針指向這個私有資料對象執行個體。

實際上不同的QObject派生類型的對象執行個體中,都會讓d_ptr指向自己獨特的私有資料對象執行個體。也就是QObject的d_ptr和QObjectData的q_ptr這兩個指針指向的對象執行個體之間存在雙向互相持有對方指針的情況。

QWidget是一個反面特例

QT架構中除了像QThread這樣嚴格遵守一個QT對象執行個體隻包含一個d_ptr範圍指針的情況之外,還有一些特例,比如QWidget就是一個不嚴格遵守這種情況的特例。

QWidget的類型定義:

QT對象類型QObject源碼中的間接的設計思想

 看一下QWidget的構造函數:

QT對象類型QObject源碼中的間接的設計思想

盡管QWidget除了QObject的d_ptr指針之外還有一個自己的指針data,但是看起來似乎仍然是符合這種不定義具體資料成員之定義指針的這種設計思想的。實際上并非如此。來看一下QPaintDevice類型的定義。

QT對象類型QObject源碼中的間接的設計思想

顯然QPaintDevice這個類型中除了一個指針之外還有非指針類型的成員變量painters。盡管QPaintDevice并非QObject派生類型,但是肯定是影響到了QWidget的記憶體布局。

3. 問題根源和解決方案

問題根源

QT架構會使用d_ptr這麼一個範圍指針,當然QT架構中還有很多類型使用的是原始指針類型。使用這麼一個指針類型顯然會帶來編碼上的一些額外的工作量,比如不能直接通路成員變量,而隻能通過指針間接通路。

既然使用指針吃力不讨好,為什麼QT架構要使用這麼一個指針呢?無利不起早,QT架構的設計者不可能吃飽了撐的沒事找事。d_ptr必然是解決了一些實際問題才有存在的價值。

先來看問題是怎麼産生的。

假定有一個類型定義在butianyunobject.h檔案中。

QT對象類型QObject源碼中的間接的設計思想

這個類型直接把私有資料成員變量定義在ButianyunObject類型本身,這在一些應用場景下會帶來一些問題。

首先是編譯問題。

如果有10個cpp檔案#include了butianyunobject.h檔案,那麼一旦對這個.h檔案的資料成員變量做那麼一丁點修改,在下一次編譯這個項目時就會導緻這個10個cpp檔案全部都必須重新編譯。這也就是直接将類型的資料成員變量定義在類型本身的缺點。

其次是依賴問題。

如果說帶來的不必要的重新編譯問題隻是一個項目組内部的技術問題,影響範圍有限,那麼現在讨論的依賴問題可能影響範圍會比較大一點。

考慮這樣一個場景:這個代碼出現在一個對外公開釋出的動态連結庫中,而且作為一個公開導出接口,很多客戶項目産品中使用了這個公開導出接口。

現在庫開發項目組中如果有人為了修複BUG,在某個新版本中修改了一下這個ButianyunObject類型的私有資料成員變量然後将這個動态連結庫和對應的頭檔案公開釋出出去,會産生什麼問題呢?或者說會影響到客戶項目産品碼?

答案是一定會影響到使用新版本的客戶項目産品。一個類型的資料成員變量修改之後,會導緻類型的對象執行個體的記憶體布局發生變化,這意味着所有想使用新版本的客戶項目産品的軟體必須使用新的頭檔案重新編譯,而無法直接使用新版本的動态連結庫去替換舊版本的動态連結庫。

解決方案

然後來看解決方案,或者說設計思路。

如果将ButianyunObject類型這麼來定義就可以避免這些問題。

QT對象類型QObject源碼中的間接的設計思想

也就是将類型的私有資料成員變量全部抽取到一個私有資料類型中,然後将這個私有資料類型定義在.cpp檔案中,而不是.h檔案中。在對外公開的資料類型中隻定義了一個指針類型的變量d_ptr,  d_ptr指向實際的私有資料對象執行個體。

這樣就算是修改私有資料成員變量,也不會導緻對外公開接口發生任何變化。也就不會引起内部編譯問題和外部依賴問題。

4. 間接的設計思想

這種設計思想就是所說的C++ PIMP。PIMP=Pointer to Implement,也就是指向具體實作的指針,說白了就是使用指向對象的具體私有資料對象執行個體的指針代替直接定義私有資料本身。

QT架構中實際上大量使用了PIMP設計思想。

下面再進一步進行抽象思考,再拔高一個層次,所謂PIMP,就是間接的思想的具體展現或者具體應用。實際上C/C++的指針類型的“指向”的含義本來自帶一層“間接”的意思。

間接的思想是所有設計模式的最本質最根本的底層設計思想和底層思考邏輯。可以這麼講,沒有哪一個設計模式不是對間接的思想的具體展現。

當然再拔高了講,大部分軟體架構也都是在某一個層面上充分應用了間接的思想。一般的思考邏輯也是将現狀抽象一下,然後在某個點上使用一個軟體架構來實作原來直接用幾行代碼去做的事情,也就是間接的使用一個更複雜的抽象邏輯架構去解決原來直接去做帶來的問題。

當然間接的思想帶來的另外一個天然的好處就是自然而然的實作了關注點分離,對于公開接口的使用者而言,根本看不到一個架構或者類的内部實作細節。