D-Pointer/bg

From Qt Wiki
Jump to: navigation, search
This article may require cleanup to meet the Qt Wiki's quality standards. Reason: Auto-imported from ExpressionEngine.
Please improve this article if you can. Remove the {{cleanup}} tag and add this page to Updated pages list after it's clean.

Български English 简体中文 Español


Какво е d-pointer

Ако някога сте гледали в сорс файловете на Qt, като този например, вероятно сте забелязали, че той е изпълнен с макросите
Q_D
и
Q_Q
. Тази статия разкрива целта на тези макроси. Макросите
Q_D
и
Q_Q
са част от подхода за софтуерен дизайн, наречен d-pointer (също известен и като непрозрачен указател), където имплементационните детайли на библиотека са скрити от потребителите и промени по имплементацията могат да се правят без да се нарушава бинарната съвместимост.

Бинарната съвметимост? Какво е това?

Когато правите дизайн на библиотеки като Qt, е желателно приложението, което динамично се свързва с библиотеката, да продължи да работи без прекомпилиране след като тя е обновена/заменена с друга нейна версия. Например, ако вашето приложение CuteApp е било базирано на Qt 4.5, трябва да можете да обновите Qt (на Windows Qt идва с приложението, на Linux често автоматично от пакетният мениджър!) от версия 4.5 до Qt 4.6 и CuteApp трябва да може да се стартира.

Какво разваля бинарната съвместимост?

Така, кога промяна в библиотеката изисква прекомпилиране на приложението? Нека погледнем този прост пример:

 class Widget {
 
 private:
 Rect m_geometry;
 };

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

Тук имаме Widget, който има член-данна за своята геометрия. Компилираме нашия Widget и го пускаме в употреба като WidgetLib 1.0.

За WidgetLib 1.1 на някой му хрумва да добави поддръжка на стилове(CSS). Няма проблем! Просто добавяме новите методи и нова член-данна.

 class Widget {
 
 private:
 Rect m_geometry;
 String m_stylesheet; // Ново в WidgetLib 1.1
 };

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

Пускаме в употреба WidgetLib 1.1 само с горната промяна и виждаме, че CuteApp, компилира и работеща добре с WidgetLib 1.0, се срива грандиозно!

Защо се срива?

Проблема е, че с добавянето на нова член-данна, ние сме променили размера на обектите от тип Widget и Label. Какво значение има това? Когато нашият C++ компилатор генерира код, той използва 'отмествания' за да достъпва данните в обектите.

Ето един доста опростен пример за това как горните POD обекти може да са разположени в паметта.

Разположение на Label в WidgetLib 1.0 Разположение на Label в WidgetLib 1.1
m_geometry <отстъп 0> m_geometry <offset 0>
————— m_stylesheet <отстъп 1>
m_text <отстъп 1> ——————
————— m_text <отстъп 2>
В !WidgetLib 1.0, m_text на Label е била на (логически) отстъп 1. Кодът генериран от компилатора в приложението за метода
Label::text()
търси текста на отстъп 1 от началото на Label обекта. В WidgetLib 1.1, член-данната text на Label е преместена на (логически) отстъп 2! Тъй като приложението не е било прекомпилирано, то продължава да мисли, че
text
е на отстъп 1 и така достъпва променливата
stylesheet
!

=== За тези, завършили C++ 101 :-)

Сигурен съм, че в този момент има няколко от вас, които се чудят защо пресмятането на отстъпа на
Label::text()
стана в CuteApp, а не в WidgetLib. Отговорът е, че кода за
Label::text()
беше дефиниран в хедър файла и компилатора го направи вмъкната функция. А тази ситуация променя ли се, ако
Label::text()
не беше вмъкнат? Да кажем, че
Label::text()
е беше преместен в сорс файла? Ами, не. C++ компилатора разчита на размера на обектите да бъде еднакъв в по време на компилация и по време на изпълнение. Например, пълненето/изпразването на стека- ако създадете обект от тип Label в стека, генерираният от компилатора код ще задели в стека място, базирайки се на размера на Label по време на компилация. Тъй като размера на Label е различен по време на изпълнение в WidgetLib 1.1, конструкторът на Label ще презапише съществуващите данни в стека, което ще доведе до прекратяване на програмата. ===

Никога не променяйте размера на експортиран C++ клас

В заключение, никога не променяйте размера или подредбата(не размествайте данните) на експортирани (т.е видими за потребителя) C++ класове веднъж щом вашата библиотека е пусната за използване. C++ компилатора генерира код като предполага, че размерът и подредбата на данните в класа не се променя след като приложението е било компилирано.

Значи, как можем да добавим нови възможности без да променим размера на обекта?

d-pointer

Номера да запазите константен размера на всички публични класове в библиотеката като пазите само един указател. Този указател сочи към частна/вътрешна структура, която съдържа данните. Размерът на тази вътрешна структура може да се свива или расте без да има странични ефекти върху приложението, защото указателят се достъпва само в библиотеката и от гледна точка на приложението размера на обекта никога не се променя - той е винаги размера да указателя. Този указател се нарича d-pointer.

Духът на този подход е показан в следния код:

 /* widget.h */
 // Предварителна декларация. Дефиницията ще бъде в widget.cpp или
 // в отделен файл, промерно widget_p.h
 class WidgetPrivate;

class Widget {
 
 Rect geometry() const;
 
 private:
 // d-pointer никога не се реферира в хедър файла.
 // Тъй като WidgetPrivate не е деклариран в този хедър,
 // всеки достъп ще бъде грешка при компилацията
 WidgetPrivate '''d_ptr;
 };

 /''' widget_p.h */ (_p означава частен (private) )
 struct WidgetPrivate {
 Rect geometry;
 String stylesheet;
 };

 /''' widget.cpp */
 #include "widget_p.h"
 Widget::Widget()
 : d_ptr(new WidgetPrivate) // създаване на частните данни {
 }

 Rect Widget::geoemtry() const {
 // d-ptr се достъпва само в кода на библиотеката
 return d_ptr->geometry;
 }

 /''' label.h */
 class Label : public Widget {
 
 String text();
 private:
 // всеки клас поддържа свой собствен d-pointer
 LabelPrivate '''d_ptr;
 };

 /''' label.cpp */
 // За разлика от WidgetPrivate, дефинираме LabelPrivate в сорс файла
 struct LabelPrivate {
 String text;
 };

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

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

С горната структура, CuteApp никога недостъпа d-pointer директно. И тъй като d-pointer се достъпва само от WidgetLib и WidgetLib се прекомпилира за всяка версия, Private класа може свободно да се променя, без това да има ефект върху CoolApp.

Други ползи от d-pointer

Не всичко е заради бинарната съвместимост. От d-pointer има и други ползи: Скрива имплементационните детайли - Можем да предоставяме WidgetLib само като хедър файлове и бинаред код. .cpp файловете могат да са затворен код.

  • Хедър файла е изчистен от имплементационни детайли и може да служи като справка за API-то на класа.
  • Тъй като хедър файловете, необходими за имплементацията са преместени от хедъра в сорс код файла, времето за компилация е доста по-малко.

Вярно е, че горните ползи изглеждат незначително, но реалната причина да се използват d-pointer-и в Qt е бинарната съвместимост и факта, че Qt е започнала като библиотека със затворен код.

q-pointer

До тук, ние разгледахме само d-pointer-и като структура от данни в стил C. В действителност, обаче те съдържат частни методи (помощни функции). Например,
LabelPrivate
може да има
getLinkTargetFromPoint()
помощна функция, която трябва да взимма линк, когато мишката е натисната. В много случаи, тези помощни функции изискват достъп до публични класове, т.е някои функции от Label или от базовия клас Widget. Например, помощен метод,
setTextAndUpdateWidget()
, може да иска да извика
Widget::update()
, който е публичен метод за заявка за прерисуване на Widget. Така че,
WidgetPrivate
пази указател към публичният клас, който се нарича q-pointer. Като модифицираме горния код, за да ползва q-pointer, получаваме:
 /* widget.h */
 // Предварителна декларация. Дефиницията ще бъде в widget.cpp или
 // в отделен файл, промерно widget_p.h
 class WidgetPrivate;

class Widget {
 
 Rect geometry() const;
 
 private:
 // d-pointer никога не се реферира в хедър файла.
 // Тъй като WidgetPrivate не е деклариран в този хедър,
 // всеки достъп ще бъде грешка при компилацията
 WidgetPrivate '''d_ptr;
 };

 /''' widget_p.h */ (_p означава частен)
 struct WidgetPrivate {
 // конструктор, който инизиализира q-ptr
 WidgetPrivate(Widget *q) : q_ptr(q) { }
 Widget '''q_ptr; // q-ptr, който сочи към клас от API-то
 Rect geometry;
 String stylesheet;
 };

 /''' widget.cpp */
 #include "widget_p.h"
 // създаване на частните данни. Подаване на 'this' указателя за да инициализира 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() const;
 private:
 LabelPrivate '''d_ptr; // всеки клас поддържа свой собствен d-pointer
 };

 /''' label.cpp */
 // За разлика от WidgetPrivate, дефинираме LabelPrivate в сорс файла
 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;
 }

По-нататъшна оптимизация

В горният код, създаването на един Label води до заделянето на памет за
LabelPrivate
и
WidgetPrivate
. Ако приложим тази стратегия за Qt, ситуацията става доста лоша за класове като
QListWidget
- той има 6 нива на вложеност и това ще доведе до 6 заделяния на памет!

Това се решева като се въведе наследяване за нашите частни класове. Така като се създаде клас d-pointer се предава нагоре по йерархията.

 /''' widget.h */
 class Widget {
 public:
 Widget();
 
 protected:
 // само подкласовете могат да достъпват долният код
 Widget(WidgetPrivate &d); // позволява на подкласовете да инициализират техните Private класове
 WidgetPrivate '''d_ptr;
 };

 /''' widget_p.h */ (_p означава частен)
 struct WidgetPrivate {
 WidgetPrivate(Widget *q) : q_ptr(q) { } // конструктор, който инициализира q-ptr
 Widget '''q_ptr; // 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 да предаде своят Private
 // забележете, че Label няма d_ptr! Просто използва този на Widget.
 };

 /''' label.cpp */
 #include "widget_p.h" // за да можем да използваме WidgetPrivate

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

Label::Label()
 : Widget(*new LabelPrivate) // инициализиране на d-pointer-а с нашият собствен Private {
 }

Label::Label(LabelPrivate &d)
 : Widget(d) {
 }
Виждате ли красотата? Сега когато създадем
Label
обект, той ще създаде
LabelPrivate
( който е подклас на
WidgetPrivate
). Той подава конкретен d-pointer към защитеният конструктор на Widget! Сега, когато
Label
обекта е създаден, има само едно заделяне на памет. Label също има защитен конструктор, който може да се използва от клас, наследяващ
Label
, за да предостави свой собствен Private клас.

Превръщане на q-ptr и d-ptr в правилният тип (QPTR и DPTR)

Страничен ефек от оптимизацията, която направихме в предната стъпка е, че q-ptr и d-ptr са от тип
Widget
и
WidgetPrivate
. Това значи, че следното няма да работи:
 void Label::setText(const String &text) {
 // няма да работи! Тъй като d_ptr е от тип WidgetPrivate, въпреки че сочи към обекто то тип LabelPrivate
 d_ptr->text = text;
 }

Следователно, когато достъпваме d-pointer в подклас, ние трябва да изпозлваме static_cast, за да превърнем указателя в правилният тип.

 void Label::setText(const String &text) {
 LabelPrivate '''d = static_cast<LabelPrivate'''>(d_ptr); // превръщане в правилният тип
 d->text = text;
 }

Както може да видите, не е много приятно да имате static_cast навсякъде из кода. За това можем да си създадем макроси:

 // global.h (macros)
 #define DPTR (Class) Class##Private '''d = static_cast<Class##Private'''>(d_ptr)
 #define QPTR (Class) Class '''q = static_cast<Class'''>(q_ptr)

// label.cpp
 void Label::setText(const String &text) {
 DPTR (Label);
 d->text = text;
 }

void LabelPrivate::someHelperFunction() {
 QPTR (label);
 q->selectAll(); // вече можем да си извикаваме функции в Label
 }

d-pointers в Qt

В Qt на практика всеки публичен клас използва d-pointer подхода. Единствените класове, в който той не е използван, са тези, за които е ясно, че никога няма да се наложи да имат допълнителни член-данни. Например, за класове като
QPoint
и
QRect
, не се очаква да се добавят нови членове, следователно данните се съхраняват директно в класа, вместо да се използва d-pointer.
  • В Qt, основният клас на всички Private обекти е
    QObjectPrivate
    
  • Макросите
    Q_D
    
    и
    Q_Q
    
    предоставят функциалността на дискутираните по-горе QPTR и DPTR.
  • Qt класовете имат макроса
    Q_DECLARE_PRIVATE
    
    в публичният клас. Макросът изглежда така:
 // qlabel.h
 class QLabel {
 private:
 Q_DECLARE_PRIVATE(QLabel);
 };

// qglobal.h
 #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;
Идеята е, че
QLabel
предостая функция
d_func()
, която позволява достъп до частният си вътрешен клас. Самият метод е частен (поради това, че макроса е в частната секция на qlabel.h).
d_func()
обаче може да се извиква от приятелски класове на
QLabel
. Това основно е полезно за достъп до информация от Qt класове, които не могат да достъпват информация от
QLabel
през публичното API. Като странне пример,
QLabel
може да пази информация за това колко пъти потребителят е натискал на линк. Обаче няма публично API за достъп до тази информация.
QStatistics
е клас, който се нуждае от тази информация. Qt разратотчика ще добави
QStatistics
като приятел на
QLabel
и тогава
QStatistics
ще може да направи
label->d_func()->linkClickCount
. Друго предимство на
d_func
е, че налага правилно използване на const. В константен метод на класа MyClass вие имате нужда от Q_D(const MyClass) и по този начин можете да извиквате само константни методи на MyClassPrivate. С "гол" d_ptr може също да извиквате и не константни методи. Също така има и
Q_DECLARE_PUBLIC
, който се използва в Private класа, за деклариране на публичния клас.