Qt源碼解析之-從PIMPL機制到d指針
- 2020 年 5 月 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;
}
上面的代碼其實並不是如何巧妙,裏面加了一些東西,可以方便其他模塊拓展,這裡只是作為一個總結和參考