关于c++:Qt中的dptr与qptr

Qt中的私有类中个别都会蕴含d_ptr这样一个公有类型的指针,指针指向该类对应的公有类,引入这个指针次要是为了解决二进制兼容的问题。

什么是二进制兼容

Qt作为一个第三方库,公布后会有很多私有类提供给第三方应用,例如QWidget这类控件类。如果Lib1.0版本中蕴含以下实现。

class Widget {
    …
private:
    Rect m_geometry;
};
class Label : public Widget {
…
    String text() const { return m_text; }
private:
    String m_text;
};

当降级为Lib2.0后,新增了m_stylesheet这样一个成员。

class Widget {
    …
private:
    Rect m_geometry;
    String m_stylesheet; // new stylesheet member
};
class Label : public Widget {
public:
    …
    String text() const { return m_text; }
private:
    String m_text;
};

如果第三方利用进行了这样的降级,须要从新编译App,否则会导致解体。究其原因,通过增加了一个新的数据成员,咱们最终扭转了 Widget 和 Label 对象的大小。为什么会这样?因为当你的C++编译器生成代码的时候,他会用偏移量来拜访对象的数据。上面是一个对象在内存外面布局的一个简化版本。| Label对象在 Lib1.0的布局| Label 对象在 Lib2.0的布局 | | m_geometry <偏移量 0> | m_geometry <偏移量 0>| | —————- | m_text <偏移量 1>| | m_stylesheet <偏移量 1> | —————- | | —————- | m_text <偏移量 2>|,在Lib 1.0中,Label的 text 成员在(逻辑)偏移量为1的地位。在编译器生成的代码里,应用程序的办法 Label::text() 被翻译成拜访 Label 对象外面偏移量为1的地位。 在Lib 2.0中,Label 的 text 成员的(逻辑)偏移量被转移到了2的地位!因为应用程序没有从新编译,它依然认为 text 在偏移量1的地位,后果却拜访了stylesheet变量!为什么Label::text()的偏移量的计算的代码会在App二进制文件执行,而不是Lib的二进制文件。 答案是因为Label::text() 的代码定义在头文件外面,最终被内联。

那么如果 Label::text() 没有定义为内联函数,状况会扭转吗?这么讲,Label::text() 被移到源文件外面?也不会。C编译器依赖对象大小在编译时和运行时雷同。比方,堆栈的 winding/unwinding – 如果你在堆栈上创立了一个Label 对象, 编译器产生的代码会依据 Label 对象在编译时的大小在堆栈上调配空间。因为Label的大小在Lib 2.0运行时曾经不同,Label 的构造函数会笼罩曾经存在的堆栈数据,最终毁坏堆栈。

因而最好不要轻易扭转导出的 C++ 类的大小或者布局(不要挪动成员)。C++ 编译器生成的代码会假设,一个类的大小和成员的程序编译后就不会扭转。

d_ptr与q_ptr
须要增加新的性能而不想扭转导出类的大小和布局,就须要通过d_ptr的形式来实现了。窍门是通过保留惟一的一个指针而放弃一个类库所有公共类的大小不变。这个指针指向一个蕴含所有数据的公有的(外部的)数据结构。内部结构的大小能够增大或者减小,而不会对应用程序带来副作用,因为指针只会被类库外面的代码拜访,从应用程序的视角来看,对象的大小并没有扭转 – 它永远是指针的大小。 这个指针被叫做 d-pointer 。

d-pointer实际上能够蕴含公有的办法(辅助函数)。例如,LabelPrivate 能够有一个getLinkTargetFromPoint() 辅助函数,这些辅助函数须要拜访私有类,也就是 Label 或者它的父类 Widget 的一些函数。比方调用一个安顿重画Widget的私有办法 Widget::update()。所以,WidgetPrivate 存储了一个指向私有类的指针,称为q-pointer。批改上边的代码引入q-pointer,能够用以下代码来进行形容。

/* widget.h */
// 前置申明. 定义在 widget.cpp 或者
// 独自的一个文件,比方 widget_p.h
class WidgetPrivate;
class Widget {
    …
    Rect geometry() const;
    …
private:
    // d-pointer never referenced in header file.
    // Since WidgetPrivate is not defined in this header,
    // any access will be a compile error
    WidgetPrivate *d_ptr;
};
/* widget_p.h */ (_p 意味着公有)
struct WidgetPrivate {
    WidgetPrivate(Widget *q) : q_ptr(q) { }
    Widget *q_ptr; // q-ptr that 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::geoemtry() const {
    // d-ptr 仅仅被类库代码拜访
    return d_ptr->geometry;
}
/* label.h */
class Label : public Widget {
    …
    String text();
private:
    // 每个类保护本人的 d-pointer
    LabelPrivate *d_ptr;
};

这里间接在cpp中定义LabelPrivate。

/* label.cpp */
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;
}

进一步改良

当上述形式在存在继承关系的类中应用时,因为Private公有的类没有建设继承关系,导致Label类进行实例化时会调配两次内存,WidgetPrivate和LabelPrivate。这里通过增加一个公有类的继承关系来解决这个问题。

/* widget.h */
class Widget {
public:
    Widget();
    …
protected:
    Widget(WidgetPrivate *d); // 容许子类通过他们本人的实体公有对象来初始化
    WidgetPrivate *d_ptr;
};

因为公有类中有须要拜访公共接口的需要,这里引入q_ptr来指向公有类对应的公共类。

/* widget_p.h */
struct WidgetPrivate {
    WidgetPrivate(Widget *q) : q_ptr(q) { }
    Widget *q_ptr; // 指向API类的
    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); // 容许Label的子类传递本人的公有数据
    //留神 Label 没有 d_ptr!它用了父类 Widget 的 d_ptr。
};
/* label.cpp */
#include "widget_p.h" // 所以咱们可能拜访 WidgetPrivate
class LabelPrivate : public WidgetPrivate 
{
public:
    LabelPrivate(Widget *q);
    String text;
};

LabelPrivate::LabelPrivate(Widget *q)
    : WidgetPrivate(q)
{

}

Label::Label()
    : Widget(new LabelPrivate(this))) {
// 用咱们本人的公有对象来初始化 d-pointer,用本人来初始化q-pointer
}

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

上一步优化的一个副作用是 q-ptr 和 d-ptr 的类型别离是 Widget和 WidgetPrivate。这就意味着须要子类类型时要进行static_cast这样的转换。

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

能够定义这样的宏来使代码变得简洁。

#define DPTR(Class) Class##Private d = static_cast<Class##Private>(d_ptr)
#define QPTR(Class) Class q = static_cast<Class>(q_ptr)

void LabelPrivate::someHelperFunction() {
    QPTR(label);
    q->selectAll(); // we can call functions in Label now
}
void Label::setText(const String &text) {
    DPTR(Label);
    d->text = text;
}

到这里就根本实现了Qt中d_ptr和q_ptr的作用。

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理