一、PIMPL机制

PIMPL ,即Private Implementation,作用是,实现 私有化,力图使得头文件对改变不透明,以达到解耦的目的

该句出自 超越 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;
}

上面的代码其实并不是如何巧妙,里面加了一些东西,可以方便其他模块拓展,这里只是作为一个总结和参考

05-10 14:32