Qt原始碼解析之-從PIMPL機制到d指標

sherlock_lin發表於2020-05-10

一、PIMPL機制

PIMPL ,即Private Implementation,作用是,實現 私有化,力圖使得標頭檔案對改變不透明,以達到解耦的目的

pimpl 用法背後的思想是把客戶與所有關於類的私有部分的知識隔離開。由於客戶是依賴於類的標頭檔案的,標頭檔案中的任何變化都會影響客戶,即使僅是對私有節或保護節的修改。pimpl用法隱藏了這些細節,方法是將私有資料和函式放入一個單獨的類中,並儲存在一個實現檔案中,然後在標頭檔案中對這個類進行前向宣告並儲存一個指向該實現類的指標。類的建構函式分配這個pimpl類,而解構函式則釋放它。這樣可以消除標頭檔案與實現細節的相關性

該句出自 超越 C++ 標準庫--boost程式庫 導論

該文的程式碼說明均忽略一些簡單必須的程式碼,以保證示例的簡潔,比如防止標頭檔案重複包含等

(1)例項說明

假設現在有一個需求,你需要寫一個類,來完成產品的資訊儲存和獲取,這個需求看起來非常的簡單,我們只需要一分鐘就能寫好

Product.h

class Product
{
public:
    string getName() const;
    void setName(const string& name);
    float getPrice() const;
    void setPrice(float price);
private:
    string name;
    float price;
};

Product.cpp

string Product::getName() const
{
    return this->name;
}
void Product::setName(const string &name)
{
    this->name = name;
}
float Product::getPrice() const
{
    return this->price;
}
void Product::setPrice(float price)
{
    this->price = price;
}

當然,你可能會說,這個簡單的程式碼根本不需要cpp,但是我們這裡只是舉個例子,實際的情況肯定比這複雜的多的多。

言歸正傳,我們完成了我們的模組,並交付出去提供給他人呼叫,結果第二天,有了新的需求,你需要新增一個成員變數,用作其中某個業務邏輯的資料儲存,所以你不得不在標頭檔案中的class內新增了一個成員屬性,並在cpp中修改邏輯,辛運的是對外開放的介面並沒有任何變動,呼叫你的模組的地方不需要修改程式碼。完成之後,交付使用。

然後這時候問題來了,呼叫此模組的人向你抱怨,替換了你的模組之後,明明自己沒有修改任何東西,但是整個工程重新編譯了整整半個多小時(可能有些誇張)。因為整個工程程式碼量巨大,很多地方都使用了你的模組,包含了你的標頭檔案,導致這些包含你的標頭檔案的地方雖然沒有變動,但是都重新編譯了。

利用PIMPL機制,將私有成員隱藏起來,使得只有介面不變,那麼標頭檔案就不會改變,已達到解耦的目的。從上面例子也可以看出,PIMPL機制的好處之一就是避免標頭檔案依賴,提高編譯速度。

那利用PIMPL機制,上面的問題如何解決呢?

(2)利用PIMPL機制

基於原來的需求,程式碼設計如下:

Product.h

class ProductData;
class Product
{
public:
    Product();
    ~Product();
    
    string getName() const;
    void setName(const string& name);
    float getPrice() const;
    void setPrice(float price);
    
private:
    ProductData* data;
};

Product.cpp

class ProductData
{
public:
    string name;
    float price;
};

Product::Product()
{
    data = new ProductData();
}
Product::~Product()
{
    delete data;
}
string Product::getName() const
{
    return data->name;
}
void Product::setName(const string &name)
{
    data->name = name;
}
float Product::getPrice() const
{
    return data->price;
}
void Product::setPrice(float price)
{
    data->price = price;
}

可以看出來,Product 類除了必要的介面函式外,就只有一個ProductData指標了,而ProductData又是使用的前置宣告,在cpp中實現,這樣,只要介面不變,那麼內部私有成員或者邏輯改變,並不會影響client

上面的程式碼只是最簡單的實現,其中還存在很多問題,而實際的專案中可能要複雜的多。儘管如此,我們也能看出PIMPL機制的優點:

  • 降低耦合度;
  • 隱藏模組資訊;
  • 降低編譯依賴,提高編譯速度;
  • 介面和實現真正分離;

二、Qt原始碼中的d指標

瞭解PIMPL機制之後,我們可以看看優秀的C++庫中是如何實現PIMPL機制的,以Qt框架為例。讀過Qt原始碼的同學對Qt中的d指標想必不會陌生,我們來詳細講解一下

(1)QThread中的PIMPL機制

我們隨便選取一個Qt中的模組,以QThread為例分析一下Qt中是如何實現PIMPL機制的

首先,找到QThread類的標頭檔案 qthread.h,我們可以看到,QThread 類的什麼中,除了對外的介面外,根本看不到能夠猜測內部實現方法或者變數,而且其private的成員只有下面幾個

class Q_CORE_EXPORT QThread : public QObject
{
    ...
private:
    Q_DECLARE_PRIVATE(QThread)

    friend class QCoreApplication;
    friend class QThreadData;
};

那麼,QThread內部使用的方法和屬性都去哪裡了呢

我們先找到它的建構函式,實現如下:

QThread::QThread(QObject *parent)
    : QObject(*(new QThreadPrivate), parent)
{
    Q_D(QThread);
    d->data->thread = this;
}

(2)Q_D巨集

Q_D() 是Qt中的一個巨集定義

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

Q_D(QThread); 展開如下:

QThreadPrivate * const d = d_func();

這也是上面程式碼中的 d 指標的由來,可以看到,d其實是一個QThreadPrivate指標,const標在d前面,型別後面,表示d指標的的指向不能改變,這點不懂的需要去複習一下const的用法,同理,q是QThread指標,且指向不能改變,所以,程式碼中出現下面的巨集將會得到傳入物件的指標

Q_D(QThread);		//QThread*
Q_D(const QThread);	//const QThread*

(3)d_func()

這裡有一個方法 d_func(),我們可以檢視到它的宣告

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

上面這段程式碼會生成方法 d_func(),而在QThread 標頭檔案類宣告中,可以看到此巨集

class Q_CORE_EXPORT QThread : public QObject
{
    ...
private:
    Q_DECLARE_PRIVATE(QThread)
	...
};

(4)Q_DECLARE_PRIVATE巨集

Q_DECLARE_PRIVATE(QThread) 巨集展開如下:

class Q_CORE_EXPORT QThread : public QObject
{
    ...
private:
	inline QThreadPrivate* d_func(){
    	return reinterpret_cast<QThreadPrivate*>(qGetPtrHelper(d_ptr));
	}
	inline const QThreadPrivate* d_func() const {
    	return reinterpret_cast<QThreadPrivate*>(qGetPtrHelper(d_ptr));
	}
	friend class QThreadPrivate;
};

可以看出,這個巨集其實是在QThread內部定義了兩個 inline 方法和一個友元類,d_func() 方法也來源於此

(5)qGetPtrHelper()方法

這裡的 qGetPtrHelper() 方法可以找到,我給它重新排版一下,如下:

template <typename T> static inline T *qGetPtrHelper(T *ptr) { 
    return ptr; 
}
template <typename Wrapper> static inline typename Wrapper::pointer qGetPtrHelper(const Wrapper &p) { 
    return p.data(); 
}

這是一個模板方法,如果只是一個普通的類指標,則返回該指標;

而如果是一個類别範本方法,則呼叫data()方法,並返回結果

所以,我們看到上面的 d_func() 方法中的,替換了 qGetPtrHelper() 方法後,如下:

class Q_CORE_EXPORT QThread : public QObject
{
    ...
	inline QThreadPrivate* d_func(){
    	return reinterpret_cast<QThreadPrivate*>(d_ptr.data());
	}
	inline const QThreadPrivate* d_func() const {
    	return reinterpret_cast<QThreadPrivate*>(d_ptr.data());
	}
};

(6)d_ptr

那麼,這裡的 d_ptr 又是哪裡來的呢,它其實是在 QObject 物件內定義的

class Q_CORE_EXPORT QObject
{
    ...
 protected:
    QScopedPointer<QObjectData> d_ptr;
};

我們都知道,Qt 中所有物件都是繼承自 QObject 的,所以 QThread 是可以使用 d_ptr 的

QScopedPointer 是Qt中封裝的智慧指標,相當於stl中的std::unique_ptr,所以,上面程式碼中的 d_ptr.data() 作用是獲取智慧指標管理的指標,等同於std::unique_ptr中的 get() 方法,也就是這裡的 QObjectData 指標。

所以,d_func() 方法的作用是,獲取 QThread 中繼承的 QObject 中的 QObjectData 指標,並使用強制型別轉換為 QThreadPrivate 指標型別。而為什麼能轉換,因為他們之間是有繼承關係的 QThreadPrivate -> QObjectPrivate -> QObjectData

上面說的所有東西最終都是在分析 Q_D(QThread);,現在我們知道,這句巨集定義最後會得到 QThreadPrivate 指標,而這個類的作用就是我們之前講的 PIMPL機制中的用作儲存 QThread 類私有成員,以達到解耦的目的

三、使用d指標

學完了Qt巧妙的d指標,我們在第一節中的程式碼中照葫蘆畫瓢引入d指標,最終程式碼如下:

global.h

template <typename T> static inline T *qGetPtrHelper(T *ptr) { return ptr; }
template <typename Wrapper> static inline typename Wrapper::pointer qGetPtrHelper(const Wrapper &p) { return p.get(); }

#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_D(Class) Class##Private * const d = d_func()
#define Q_Q(Class) Class * const q = q_func()

base.h

class BaseData
{
public:
    BaseData(){}
    virtual ~BaseData(){}
};

class BasePrivate : public BaseData
{
public:
    BasePrivate(){}
    virtual ~BasePrivate() {}
};

product.h

#include "global.h"
#include "test_p.h"
#include <memory>
#include <string>

using namespace std;

class ProductPrivate;
class ProductData;

class Product
{
public:
    explicit Product(int num = 1);
    ~Product();
    string getName() const;
    void setName(const string& name);
    float getPrice() const;
    void setPrice(float price);
protected:
    Product(ProductPrivate* testPrivate, int num = 1);
protected:
    std::unique_ptr<BaseData> d_ptr;
private:
    Q_DECLARE_PRIVATE(Product)
    friend class ProductData;
};

product.cpp

#include "product.h"
#include <iostream>
using namespace std;

class ProductData : public BaseData
{
public:
    Product* test;
};

class ProductPrivate : public BasePrivate
{
public:
    ProductPrivate(ProductData* d = 0) : data(d) {
        if(!data) {
            data = new ProductData();
        }
    }
    ProductData* data;

    int number;
    string name;
    float price;
};

Product::Product(ProductPrivate *testPrivate, int num) : d_ptr(testPrivate)
{
    Q_D(Product);
    d->data->test = this;
}

Product::Product(int num)
{
    d_ptr = std::unique_ptr<ProductPrivate>(new ProductPrivate());
}

string Product::getName() const
{
    Q_D(const Product);
    return d->name;
}

void Product::setName(const string &name)
{
    Q_D(Product);
    d->name = name;
}

float Product::getPrice() const
{
    Q_D(const Product);
    return d->price;
}

void Product::setPrice(float price)
{
    Q_D(Product);
    d->price = price;
}

上面的程式碼其實並不是如何巧妙,裡面加了一些東西,可以方便其他模組擴充,這裡只是作為一個總結和參考

相關文章