QT中的d-pointer模式分析

1. 概述

类中的数据有多种组织方式,通常存放在类本身中,有时,也会将数据部分分离出来,放在另外一个结构体或类中,只在类本身中存放一个指向该结构体的指针。如下图所示:

QT中的d-pointer模式分析_第1张图片

这样做的好处是:

  1. 隐藏实现,可以直接发布头文件作为接口文档而不必担心源码泄露;
  2. 降低编译依赖,加快了编译速度;

当然,分离数据也有其缺点:

  1. 每次函数调用都涉及到指针操作,程序运行速度可能变慢;
  2. 需要在堆上开辟空间

这种模式一般被称为pimpl(pointer to implementation), Qt中引入了这种模式,在Qt文档中通常称为d-pointer. d-pointer在Qt中主要用来维护Qt库的二进制兼容性,同时d-pointer在隐式数据共享(Copy-On-Write)也可以发挥重要的作用,本文首先分析d-pointer的实现原理,然后结合例子分析d-pointer如何维护Qt的二进制兼容性以及实现隐式共享。

2. d-pointer实现原理

一般的pimpl模式的实现如下, Qt的参考文档中给出了一个简洁的例子:

widget.h

 class WidgetPrivate;
 
 class Widget
 {
     // ...
     Rect geometry() const;
     // ... 
 
 private:
     WidgetPrivate *d_ptr;
 };

widget_p.h,widget 类内部使用的私有头文件

/* widget_p.h (_p means private) */
struct WidgetPrivate
{
    Rect geometry;
    String stylesheet;
};

widget.cpp:

// With this #include, we can access WidgetPrivate.
#include "widget_p.h"

Widget::Widget() : d_ptr(new WidgetPrivate)
{
    // Creation of private data
}

Rect Widget::geometry() const
{
    // The d-ptr is only accessed in the library code
    return d_ptr->geometry;
}

上面的例子中,所有的数据都存放在WidgetPrivate中,在Widget类中,通过指针d_ptr来访问这些数据。

对于有继承出现的情况,上面的方法同样适用:

label.h

class Label : public Widget
{
    // ...
    String text();

private:
    // Each class maintains its own d-pointer
    LabelPrivate *d_ptr;
};

label.cpp

// Unlike WidgetPrivate, the author decided LabelPrivate
// to be defined in the source file itself
struct LabelPrivate
{
    String text;
};

Label::Label() : d_ptr(new LabelPrivate)
{
}

String Label::text()
{
    return d_ptr->text;
}

这种方法实现了pimpl模式,但是它存着两个很明显的缺点:

  1. 在类的私有数据部分(LabelPrivate)不能访问Label或Widget的公共函数: 例如LabelPrivatge可能存着这样的接口setTextAndUpdateWidget,当用户设置了某个Label后,需要调用Widget的update函数。
  2. 每个类都有自己的私有数据成员,随着继承层次的加深,创建一个继承层次为N的继承类时将会有N+1次昂贵的堆上内存申请和释放。

对于第一个缺点,我们可以用下面的方法进行修正:传递公有类的指针到私有数据类,作为私有数据类的一个成员,这样我们就可以在私有数据类调用公有类的方法。

在私有数据类中指向公有类的指针一般被称作q-pointer.

widget.h

class WidgetPrivate;

class Widget
{
    // ...
    Rect geometry() const;
    // ...
private:
    WidgetPrivate *d_ptr;
};

widget_p.h

struct WidgetPrivate
{
    // Constructor that initializes the q-ptr
    WidgetPrivate(Widget *q) : q_ptr(q) { }
    Widget *q_ptr; // q-ptr points to the API class
    Rect geometry;
    String stylesheet;
};

widget.cpp

#include "widget_p.h"
// Create private data.
// Pass the 'this' pointer to initialize the q-ptr
Widget::Widget() : d_ptr(new WidgetPrivate(this))
{
}

Rect Widget::geometry() const
{
    // the d-ptr is only accessed in the library code
    return d_ptr->geometry;
}

对于Label类,可以做类似的改动:

label.h

class Label : public Widget
{
    // ...
    String text() const;

private:
    LabelPrivate *d_ptr;
};

label.cpp

// Unlike WidgetPrivate, the author decided LabelPrivate
// to be defined in the source file itself
struct LabelPrivate
{
    LabelPrivate(Label *q) : q_ptr(q) { }
    Label *q_ptr;
    String text;
};

Label::Label() : d_ptr(new LabelPrivate(this))
{
}

String Label::text()
{
    return d_ptr->text;
}

对于第二个缺点,我们发现在继承体系中各子类中私有数据类各自申请独立内存空间其实不是必须的,一个比较好的办法是让子类的私有数据部分也形成继承关系,这样各私有数据类就可以共享一片内存区域:

QT中的d-pointer模式分析_第2张图片

相关示例代码如下:

widget.h

class Widget
{
public:
    Widget();
    // ...
protected:
    // only subclasses may access the below
    // allow subclasses to initialize with their own concrete Private
    Widget(WidgetPrivate &d);
    WidgetPrivate *d_ptr;
};

widget_p.h

struct WidgetPrivate
{
    WidgetPrivate(Widget *q) : q_ptr(q) { } // constructor that initializes the q-ptr
    Widget *q_ptr; // q-ptr that points to the API class
    Rect geometry;
    String stylesheet;
};

widget.cpp

Widget::Widget() : d_ptr(new WidgetPrivate(this))
{
}

Widget::Widget(WidgetPrivate &d) : d_ptr(&d)
{
}

label.h

class Label : public Widget
{
public:
    Label();
    // ...
protected:
    Label(LabelPrivate &d); // allow Label subclasses to pass on their Private
    // notice how Label does not have a d_ptr! It just uses Widget's d_ptr.
};


label.cpp

#include "widget_p.h"

class LabelPrivate : public WidgetPrivate
{
public:
    String text;
};

Label::Label()
 : Widget(*new LabelPrivate) // initialize the d-pointer with our own Private
{
}

Label::Label(LabelPrivate &d) : Widget(d)
{
}

这样,当我们创建一个Label对象时,将会调用Label的构造函数创建一个LabelPrivate对象,并用该对象的地址初始化其父类的私有数据成员,从而避免了多次堆上内存申请.

当然这样做的缺点是Label类中的d_ptr和LabelPrivate中的q_ptr分别继承自Widget类与WidgetPrivate,它们的属性分别是WidgetPrivate和Widget,这样,我们在Label类中使用d_ptr和q_ptr时,必须做相应的类型转换,下面的代码是不work的:

void Label::setText(const String &text)
{
   // won't work! since d_ptr is of type WidgetPrivate even though
   // it points to LabelPrivate object
   d_ptr->text = text;
}
我们在子类Label中必须做类似的类型转换:

void Label::setText(const String &text)
{
    LabelPrivate *d = static_cast(d_ptr); // cast to our private type
    d->text = text;
}

正如你看到的那样,代码中经常使用static_cast是很难看,并且也很容易犯错,在Qt的源码corelib/global/qglobal.h中,定义了两个宏Q_D, Q_Q, 分别用来获得转型后的d_ptr和q_ptr,需要使用d_ptr和q_ptr的地方,只要简单的用d和q代替即可了:

#define Q_D(Class) Class##Private * const d = d_func()
#define Q_Q(Class) Class * const q = q_func()
而d_func和q_func在qglobal.h中是这样定义的:

#define Q_DECLARE_PRIVATE(Class) \
    inline Class##Private* d_func() { return reinterpret_cast(qGetPtrHelper(d_ptr)); } \
    inline const Class##Private* d_func() const { return reinterpret_cast(qGetPtrHelper(d_ptr)); } \
    friend class Class##Private;

 
  

 
  

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

其中, qGetPtrHelper只是一个简单的类型转换宏:

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

这样,我们在Label中需要用到d_ptr和q_ptr的地方就可以这样用Q_D和Q_Q来进行简化:

// With Q_D you can use the members of LabelPrivate from Label
void Label::setText(const String &text)
{
    Q_D(Label);
    d->text = text;
}

// With Q_Q you can use the members of Label from LabelPrivate
void LabelPrivate::someHelperFunction()
{
    Q_Q(Label);
    q->selectAll();
}


d-pointer相关的代码不仅仅只能用到Qt相关的代码,我们也可以将它们从Qt中剥离出来,用到一般的C/C++代码中,这里有一个类似的开源实现:

#define DPTR_INIT(p) dptr(p)
//put in protected
#define DPTR_DECLARE(Class) DPtrInterface dptr;
//put in private
#define DPTR_DECLARE_PRIVATE(Class) \
    inline Class##Private& d_func() { return dptr.pri(); } \
    inline const Class##Private& d_func() const { return dptr.pri(); } \
    friend class Class##Private;

#define DPTR_DECLARE_PUBLIC(Class) \
    inline Class& q_func() { return *static_cast(dptr_ptr()); } \
    inline const Class& q_func() const { return *static_cast(dptr_ptr()); } \
    friend class Class;

#define DPTR_INIT_PRIVATE(Class) dptr.setPublic(this);
#define DPTR_D(Class) Class##Private& d = dptr.pri()
#define DPTR_P(Class) Class& p = *static_cast(dptr_ptr())

//interface
template 
class DPtrPrivate
{
public:
    virtual ~DPtrPrivate() {}
    inline void DPTR_setPublic(PUB* pub) { dptr_p_ptr = pub; }
protected:
    inline PUB& dptr_p() { return *dptr_p_ptr; }
    inline const PUB& dptr_p() const { return *dptr_p_ptr; }
    inline PUB* dptr_ptr() { return dptr_p_ptr; }
    inline const PUB* dptr_ptr() const { return dptr_p_ptr; }
private:
    PUB* dptr_p_ptr;
};

//interface
template 
class DPtrInterface
{
    friend class DPtrPrivate;
public:
    DPtrInterface(PVT* d):pvt(d) {}
    DPtrInterface():pvt(new PVT) {}
    ~DPtrInterface() {
        if (pvt) {
            delete pvt;
            pvt = 0;
        }
    }
    inline void setPublic(PUB* pub) { pvt->DPTR_setPublic(pub); }
    template 
    inline T& pri() { return *reinterpret_cast(pvt); }
    template 
    inline const T& pri() const { return *reinterpret_cast(pvt); } //static cast requires defination of T
    inline PVT& operator()() { return *static_cast(pvt); }
    inline const PVT& operator()() const { return *static_cast(pvt); }
    inline PVT * operator->() { return static_cast(pvt); }
    inline const PVT * operator->() const { return static_cast(pvt); }
private:
    DPtrInterface(const DPtrInterface&);
    DPtrInterface& operator=(const DPtrInterface&);
    DPtrPrivate* pvt;
};


3. 采用d-pointer维护Qt的二进制兼容性

什么是二进制兼容呢?在这里可以看到其定义:

A library is binary compatible,if a program linked dynamically to a former version of the library continues running with newer versions of the library without the need to recompile.


二进制兼容的好处是当新库更新后,我们可以直接用新库替换旧库而不用重新编译、部署应用程序。当然这只限于动态链接库。编写代码时,保证库的二进制兼容的一条基本规则是:对于公有类,一旦发布,后期的版本只能在其上添加新的静态数据成员;不可以添加、删除非静态数据成员,不可以更改非静态数据成员的定义顺序和类型等。对于私有类,则没有任何的限制。主要原因是C++编译器采用offset来获取数据,当我们改变类的大小或数据相对位置时,同一offset处对应的数据成员会不同。

因而,采用上面的d-pointer则可以很好的维护Qt的二进制兼容性。在这篇文章中,也介绍了其它方法实现二进制兼容。


4. 采用d-pointer实现隐式共享(Copy-On-Write)

这里采用《Qt中的C++技术》一书中的例子来说明如何采用d-pointer实现隐式共享。如果需要定义一个类来实现3X3的矩阵存储与操作,传统的定义方式如下:矩阵存放在类Matrix的私有数据成员data中,每个Matrix对象被创建时就拥有这样的一个数据区域,即使多个对象具有完全相同的矩阵元素,我们也无法实现共享同一块内存区域。

class Matrix {
public:
    Matrix() {
        memset(data, 0, sizeof(data));
    ~Matrix();

private:
    /* data */
    double data[3][3];
};

采用d_pointer模式加上智能指针可以实现copy-on-write:

#include 
#include 
using namespace std;

class Matrix;
class MatrixData {
    friend class Matrix;
public:
    MatrixData() {
        memset(data, 0, sizeof(data));
    }
private:
    int refCount;
    double data[3][3];
};

class Matrix {
public:
    Matrix() {
        d = new MatrixData;
        d->refCount = 1;
    }
    Matrix(const Matrix& other) {
        d = other.d;
        d->refCount++;
    }
    ~Matrix() {
        if (--d->refCount == 0) {
            delete d;
        }
    }

    double& operator()(int row, int col) {
        detach();
        return d->data[row][col];
    }



    void detach() {
        if (d->refCount <= 1) return;
        d->refCount--;
        d = new MatrixData(*d);
        d->refCount = 1;
    }

private:
    MatrixData* d;
};

int main(int argc, const char *argv[]) {
    Matrix m1;
    Matrix m2(m1), m3(m1);

    m3(0, 0) = 10;
    return 0;
}


你可能感兴趣的:(C++,qt,d-pointer)