D-Pointer/bg
Български English 简体中文 Español
[toc align_right="yes" depth="3"]
Какво е d-pointer
Ако някога сте гледали в сорс файловете на Qt, "като този например":http://qt.gitorious.com/qt/qt/blobs/master/src/gui/widgets/qlabel.cpp, вероятно сте забелязали, че той е изпълнен с макросите <code>Q_D</code> и <code>Q_Q</code>. Тази статия разкрива целта на тези макроси.
Макросите <code>Q_D</code> и <code>Q_Q</code> са част от подхода за софтуерен дизайн, наречен d-pointer (също известен и като "непрозрачен указател":http://en.wikipedia.org/wiki/Opaque_pointer), където имплементационните детайли на библиотека са скрити от потребителите и промени по имплементацията могат да се правят без да се нарушава бинарната съвместимост.
Бинарната съвметимост? Какво е това?
Когато правите дизайн на библиотеки като Qt, е желателно приложението, което динамично се свързва с библиотеката, да продължи да работи без прекомпилиране след като тя е обновена/заменена с друга нейна версия. Например, ако вашето приложение CuteApp е било базирано на Qt 4.5, трябва да можете да обновите Qt (на Windows Qt идва с приложението, на Linux често автоматично от пакетният мениджър!) от версия 4.5 до Qt 4.6 и CuteApp трябва да може да се стартира.
Какво разваля бинарната съвместимост?
Така, кога промяна в библиотеката изисква прекомпилиране на приложението? Нека погледнем този прост пример:
<br /> class Widget {<br /> …<br /> private:<br /> Rect m_geometry;<br /> };
class Label : public Widget {<br /> …<br /> String text() const { return m_text; }<br /> private:<br /> String m_text;<br /> };<br />
Тук имаме Widget, който има член-данна за своята геометрия. Компилираме нашия Widget и го пускаме в употреба като WidgetLib 1.0.
За WidgetLib 1.1 на някой му хрумва да добави поддръжка на стилове(CSS). Няма проблем! Просто добавяме новите методи и нова член-данна.
<br /> class Widget {<br /> …<br /> private:<br /> Rect m_geometry;<br /> String m_stylesheet; // Ново в WidgetLib 1.1<br /> };
class Label : public Widget {<br /> public:<br /> …<br /> String text() const { return m_text; }<br /> private:<br /> String m_text;<br /> };<br />
Пускаме в употреба 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. Кодът генериран от компилатора в приложението за метода <code>Label::text()</code> търси текста на отстъп 1 от началото на Label обекта. В WidgetLib 1.1, член-данната text на Label е преместена на (логически) отстъп 2! Тъй като приложението не е било прекомпилирано, то продължава да мисли, че <code>text</code> е на отстъп 1 и така достъпва променливата <code>stylesheet</code>!
=== За тези, завършили C++ 101 :)
ако създадете обект от тип Label в стека, генерираният от компилатора код ще задели в стека място, базирайки се на размера на Label по време на компилация. Тъй като размера на Label е различен по време на изпълнение в WidgetLib 1.1, конструкторът на Label ще презапише съществуващите данни в стека, което ще доведе до прекратяване на програмата. ===
Сигурен съм, че в този момент има няколко от вас, които се чудят защо пресмятането на отстъпа на <code>Label::text()</code> стана в CuteApp, а не в WidgetLib. Отговорът е, че кода за <code>Label::text()</code> беше дефиниран в хедър файла и компилатора го направи "вмъкната функция":http://en.wikipedia.org/wiki/Inline_function.
А тази ситуация променя ли се, ако <code>Label::text()</code> не беше вмъкнат? Да кажем, че <code>Label::text()</code> е беше преместен в сорс файла? Ами, не. C++ компилатора разчита на размера на обектите да бъде еднакъв в по време на компилация и по време на изпълнение. Например, пълненето/изпразването на стека
Никога не променяйте размера на експортиран C++ клас
В заключение, никога не променяйте размера или подредбата(не размествайте данните) на експортирани (т.е видими за потребителя) C++ класове веднъж щом вашата библиотека е пусната за използване. C++ компилатора генерира код като предполага, че размерът и подредбата на данните в класа не се променя след като приложението е било компилирано.
Значи, как можем да добавим нови възможности без да променим размера на обекта?
d-pointer
Номера да запазите константен размера на всички публични класове в библиотеката като пазите само един указател. Този указател сочи към частна/вътрешна структура, която съдържа данните. Размерът на тази вътрешна структура може да се свива или расте без да има странични ефекти върху приложението, защото указателят се достъпва само в библиотеката и от гледна точка на приложението размера на обекта никога не се променя - той е винаги размера да указателя. Този указател се нарича d-pointer.
Духът на този подход е показан в следния код:
<br /> /* widget.h */<br /> // Предварителна декларация. Дефиницията ще бъде в widget.cpp или<br /> // в отделен файл, промерно widget_p.h<br /> class WidgetPrivate;
class Widget {<br /> …<br /> Rect geometry() const;<br /> …<br /> private:<br /> // d-pointer никога не се реферира в хедър файла.<br /> // Тъй като WidgetPrivate не е деклариран в този хедър,<br /> // всеки достъп ще бъде грешка при компилацията<br /> WidgetPrivate '''d_ptr;<br /> };
<br /> /''' widget_p.h '''/ (_p означава частен (private) )<br /> struct WidgetPrivate {<br /> Rect geometry;<br /> String stylesheet;<br /> };
<br /> /''' widget.cpp '''/<br /> #include "widget_p.h&quot;<br /> Widget::Widget()<br /> : d_ptr(new WidgetPrivate) // създаване на частните данни {<br /> }
<br /> Rect Widget::geoemtry() const {<br /> // d-ptr се достъпва само в кода на библиотеката<br /> return d_ptr->geometry;<br /> }
<br /> /''' label.h */<br /> class Label : public Widget {<br /> …<br /> String text();<br /> private:<br /> // всеки клас поддържа свой собствен d-pointer<br /> LabelPrivate '''d_ptr;<br /> };
<br /> /''' label.cpp '''/<br /> // За разлика от WidgetPrivate, дефинираме LabelPrivate в сорс файла<br /> struct LabelPrivate {<br /> String text;<br /> };
<br /> Label::Label()<br /> : d_ptr(new LabelPrivate) {<br /> }
<br /> String Label::text() {<br /> return d_ptr->text;<br /> }<br />
С горната структура, CuteApp никога недостъпа d-pointer директно. И тъй като d-pointer се достъпва само от WidgetLib и WidgetLib се прекомпилира за всяка версия, Private класа може свободно да се променя, без това да има ефект върху CoolApp.
h2. Други ползи от d-pointer
Не всичко е заради бинарната съвместимост. От d-pointer има и други ползи:
Скрива имплементационните детайли - Можем да предоставяме WidgetLib само като хедър файлове и бинаред код. .cpp файловете могат да са затворен код.
* Хедър файла е изчистен от имплементационни детайли и може да служи като справка за API-то на класа.
* Тъй като хедър файловете, необходими за имплементацията са преместени от хедъра в сорс код файла, времето за компилация е доста по-малко.
Вярно е, че горните ползи изглеждат незначително, но реалната причина да се използват d-pointer-и в Qt е бинарната съвместимост и факта, че Qt е започнала като библиотека със затворен код.
q-pointer
До тук, ние разгледахме само d-pointer-и като структура от данни в стил C. В действителност, обаче те съдържат частни методи (помощни функции). Например, <code>LabelPrivate</code> може да има <code>getLinkTargetFromPoint()</code> помощна функция, която трябва да взимма линк, когато мишката е натисната. В много случаи, тези помощни функции изискват достъп до публични класове, т.е някои функции от Label или от базовия клас Widget. Например, помощен метод, <code>setTextAndUpdateWidget()</code>, може да иска да извика <code>Widget::update()</code>, който е публичен метод за заявка за прерисуване на Widget. Така че, <code>WidgetPrivate</code> пази указател към публичният клас, който се нарича q-pointer. Като модифицираме горния код, за да ползва q-pointer, получаваме:
<br /> /* widget.h */<br /> // Предварителна декларация. Дефиницията ще бъде в widget.cpp или<br /> // в отделен файл, промерно widget_p.h<br /> class WidgetPrivate;
class Widget {<br /> …<br /> Rect geometry() const;<br /> …<br /> private:<br /> // d-pointer никога не се реферира в хедър файла.<br /> // Тъй като WidgetPrivate не е деклариран в този хедър,<br /> // всеки достъп ще бъде грешка при компилацията<br /> WidgetPrivate '''d_ptr;<br /> };
<br /> /''' widget_p.h */ (_p означава частен)<br /> struct WidgetPrivate {<br /> // конструктор, който инизиализира q-ptr<br /> WidgetPrivate(Widget *q) : q_ptr(q) { }<br /> Widget '''q_ptr; // q-ptr, който сочи към клас от API-то<br /> Rect geometry;<br /> String stylesheet;<br /> };
<br /> /''' widget.cpp '''/<br /> #include "widget_p.h&quot;<br /> // създаване на частните данни. Подаване на 'this' указателя за да инициализира q-ptr<br /> Widget::Widget()<br /> : d_ptr(new WidgetPrivate(this)) {<br /> }
<br /> Rect Widget::geoemtry() const {<br /> // d-ptr се достъпва само в кода на библиотеката<br /> return d_ptr->geometry;<br /> }
<br /> /''' label.h */<br /> class Label : public Widget {<br /> …<br /> String text() const;<br /> private:<br /> LabelPrivate '''d_ptr; // всеки клас поддържа свой собствен d-pointer<br /> };
<br /> /''' label.cpp */<br /> // За разлика от WidgetPrivate, дефинираме LabelPrivate в сорс файла<br /> struct LabelPrivate {<br /> LabelPrivate(Label *q) : q_ptr(q) { }<br /> Label '''q_ptr;<br /> String text;<br /> };
<br /> Label::Label()<br /> : d_ptr(new LabelPrivate(this)) {<br /> }
<br /> String Label::text() {<br /> return d_ptr->text;<br /> }<br />
h2. По-нататъшна оптимизация
В горният код, създаването на един Label води до заделянето на памет за <code>LabelPrivate</code> и <code>WidgetPrivate</code>. Ако приложим тази стратегия за Qt, ситуацията става доста лоша за класове като <code>QListWidget</code> - той има 6 нива на вложеност и това ще доведе до 6 заделяния на памет!
Това се решева като се въведе наследяване за нашите частни класове. Така като се създаде клас d-pointer се предава нагоре по йерархията.
<br /> /''' widget.h */<br /> class Widget {<br /> public:<br /> Widget();<br /> …<br /> protected:<br /> // само подкласовете могат да достъпват долният код<br /> Widget(WidgetPrivate &d); // позволява на подкласовете да инициализират техните Private класове<br /> WidgetPrivate '''d_ptr;<br /> };
<br /> /''' widget_p.h */ (_p означава частен)<br /> struct WidgetPrivate {<br /> WidgetPrivate(Widget *q) : q_ptr(q) { } // конструктор, който инициализира q-ptr<br /> Widget '''q_ptr; // q-ptr, който сочи към API класа<br /> Rect geometry;<br /> String stylesheet;<br /> };
<br /> /''' widget.cpp '''/<br /> Widget::Widget()<br /> : d_ptr(new WidgetPrivate(this)) {<br /> }
<br /> Widget::Widget(WidgetPrivate &d)<br /> : d_ptr(&d) {<br /> }
<br /> /''' label.h '''/<br /> class Label : public Widget {<br /> public:<br /> Label();<br /> …<br /> protected:<br /> Label(LabelPrivate &d); // позволява на подкалса Label да предаде своят Private<br /> // забележете, че Label няма d_ptr! Просто използва този на Widget.<br /> };
<br /> /''' label.cpp */<br /> #include "widget_p.h&quot; // за да можем да използваме WidgetPrivate
class LabelPrivate : public WidgetPrivate {<br /> public:<br /> String text;<br /> };
Label::Label()<br /> : Widget(*new LabelPrivate) // инициализиране на d-pointer-а с нашият собствен Private {<br /> }
Label::Label(LabelPrivate &d)<br /> : Widget(d) {<br /> }<br />
Виждате ли красотата? Сега когато създадем <code>Label</code> обект, той ще създаде <code>LabelPrivate</code> ( който е подклас на <code>WidgetPrivate</code>). Той подава конкретен d-pointer към защитеният конструктор на Widget! Сега, когато <code>Label</code> обекта е създаден, има само едно заделяне на памет. Label също има защитен конструктор, който може да се използва от клас, наследяващ <code>Label</code>, за да предостави свой собствен Private клас.
Превръщане на q-ptr и d-ptr в правилният тип (QPTR и DPTR)
Страничен ефек от оптимизацията, която направихме в предната стъпка е, че q-ptr и d-ptr са от тип <code>Widget</code> и <code>WidgetPrivate</code>. Това значи, че следното няма да работи:
<br /> void Label::setText(const String &text) {<br /> // няма да работи! Тъй като d_ptr е от тип WidgetPrivate, въпреки че сочи към обекто то тип LabelPrivate<br /> d_ptr->text = text;<br /> }<br />
Следователно, когато достъпваме d-pointer в подклас, ние трябва да изпозлваме static_cast, за да превърнем указателя в правилният тип.
<br /> void Label::setText(const String &text) {<br /> LabelPrivate '''d = static_cast&lt;LabelPrivate'''>(d_ptr); // превръщане в правилният тип<br /> d->text = text;<br /> }<br />
Както може да видите, не е много приятно да имате static_cast навсякъде из кода. За това можем да си създадем макроси:
<br /> // global.h (macros)<br /> #define DPTR (Class) Class##Private '''d = static_cast&lt;Class##Private'''>(d_ptr)<br /> #define QPTR (Class) Class '''q = static_cast&lt;Class'''>(q_ptr)
// label.cpp<br /> void Label::setText(const String &text) {<br /> DPTR (Label);<br /> d->text = text;<br /> }
void LabelPrivate::someHelperFunction() {<br /> QPTR (label);<br /> q->selectAll(); // вече можем да си извикаваме функции в Label<br /> }<br />
d-pointers в Qt
В Qt на практика всеки публичен клас използва d-pointer подхода. Единствените класове, в който той не е използван, са тези, за които е ясно, че никога няма да се наложи да имат допълнителни член-данни. Например, за класове като <code>QPoint</code> и <code>QRect</code>, не се очаква да се добавят нови членове, следователно данните се съхраняват директно в класа, вместо да се използва d-pointer.
- В Qt, основният клас на всички Private обекти е <code>QObjectPrivate</code>
- Макросите <code>Q_D</code> и <code>Q_Q</code> предоставят функциалността на дискутираните по-горе QPTR и DPTR.
- Qt класовете имат макроса <code>Q_DECLARE_PRIVATE</code> в публичният клас. Макросът изглежда така:
<br /> // qlabel.h<br /> class QLabel {<br /> private:<br /> Q_DECLARE_PRIVATE(QLabel);<br /> }; // qglobal.h<br /> #define Q_DECLARE_PRIVATE(Class) inline Class##Private* d_func() { return reinterpret_cast&lt;Class##Private '''>(qGetPtrHelper(d_ptr)); } inline const Class##Private''' d_func() const { return reinterpret_cast&lt;const Class##Private *>(qGetPtrHelper(d_ptr)); } friend class Class##Private;<br />
Идеята е, че <code>QLabel</code> предостая функция <code>d_func()</code>, която позволява достъп до частният си вътрешен клас. Самият метод е частен (поради това, че макроса е в частната секция на qlabel.h). <code>d_func()</code> обаче може да се извиква от приятелски класове на <code>QLabel</code>. Това основно е полезно за достъп до информация от Qt класове, които не могат да достъпват информация от <code>QLabel</code> през публичното API. Като странне пример, <code>QLabel</code> може да пази информация за това колко пъти потребителят е натискал на линк. Обаче няма публично API за достъп до тази информация. <code>QStatistics</code> е клас, който се нуждае от тази информация. Qt разратотчика ще добави <code>QStatistics</code> като приятел на <code>QLabel</code> и тогава <code>QStatistics</code> ще може да направи <code>label->d_func()->linkClickCount</code>.
Друго предимство на <code>d_func</code> е, че налага правилно използване на const. В константен метод на класа MyClass вие имате нужда от Q_D(const MyClass) и по този начин можете да извиквате само константни методи на MyClassPrivate. С "гол" d_ptr може също да извиквате и не константни методи.
Също така има и <code>Q_DECLARE_PUBLIC</code>, който се използва в Private класа, за деклариране на публичния клас.