D-Pointer/bg: Difference between revisions
No edit summary |
No edit summary |
||
Line 1: | Line 1: | ||
[[Category:Howto]]<br />[[Category:Developing with Qt]]<br />[[Category:QtInternals]] | |||
'''Български''' [[Dpointer|English]] [[Dpointer SimplifiedChinese|简体中文]] [[Dpointer_Spanish|Español]] | |||
[toc align_right="yes&quot; depth="3&quot;] | |||
= Какво е d-pointer = | |||
==Бинарната съвметимост? Какво е това?== | Ако някога сте гледали в сорс файловете на Qt, "като този например&quot;:http://qt.gitorious.com/qt/qt/blobs/master/src/gui/widgets/qlabel.cpp, вероятно сте забелязали, че той е изпълнен с макросите <code&gt;Q_D&lt;/code&gt; и <code&gt;Q_Q&lt;/code&gt;. Тази статия разкрива целта на тези макроси. | ||
Макросите <code&gt;Q_D&lt;/code&gt; и <code&gt;Q_Q&lt;/code&gt; са част от подхода за софтуерен дизайн, наречен ''d-pointer'' (също известен и като "непрозрачен указател&quot;:http://en.wikipedia.org/wiki/Opaque_pointer), където имплементационните детайли на библиотека са скрити от потребителите и промени по имплементацията могат да се правят без да се нарушава бинарната съвместимост. | |||
== Бинарната съвметимост? Какво е това? == | |||
Когато правите дизайн на библиотеки като Qt, е желателно приложението, което динамично се свързва с библиотеката, да продължи да работи без прекомпилиране след като тя е обновена/заменена с друга нейна версия. Например, ако вашето приложение ''CuteApp'' е било базирано на Qt 4.5, трябва да можете да обновите Qt (на Windows Qt идва с приложението, на Linux често автоматично от пакетният мениджър!) от версия 4.5 до Qt 4.6 и CuteApp трябва да може да се стартира. | Когато правите дизайн на библиотеки като Qt, е желателно приложението, което динамично се свързва с библиотеката, да продължи да работи без прекомпилиране след като тя е обновена/заменена с друга нейна версия. Например, ако вашето приложение ''CuteApp'' е било базирано на Qt 4.5, трябва да можете да обновите Qt (на Windows Qt идва с приложението, на Linux често автоматично от пакетният мениджър!) от версия 4.5 до Qt 4.6 и CuteApp трябва да може да се стартира. | ||
===Какво разваля бинарната съвместимост?=== | === Какво разваля бинарната съвместимост? === | ||
Така, кога промяна в библиотеката изисква прекомпилиране на приложението? Нека погледнем този прост пример:<br /> | Така, кога промяна в библиотеката изисква прекомпилиране на приложението? Нека погледнем този прост пример:<br /><code><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 /></code> | |||
Тук имаме ''Widget'', който има член-данна за своята геометрия. Компилираме нашия Widget и го пускаме в употреба като '''WidgetLib 1.0'''. | Тук имаме ''Widget'', който има член-данна за своята геометрия. Компилираме нашия Widget и го пускаме в употреба като '''WidgetLib 1.0'''. | ||
За '''WidgetLib 1.1''' на някой му хрумва да добави поддръжка на стилове( | За '''WidgetLib 1.1''' на някой му хрумва да добави поддръжка на стилове(CSS). Няма проблем! Просто добавяме новите методи и нова ''член-данна''.<br /><code><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 /></code> | |||
Пускаме в употреба WidgetLib 1.1 само с горната промяна и виждаме, че CuteApp, компилира и работеща добре с WidgetLib 1.0, се срива грандиозно! | Пускаме в употреба WidgetLib 1.1 само с горната промяна и виждаме, че CuteApp, компилира и работеща добре с WidgetLib 1.0, се срива грандиозно! | ||
==Защо се срива?== | == Защо се срива? == | ||
Проблема е, че с добавянето на нова член-данна, ние сме променили размера на обектите от тип Widget и Label. Какво значение има това? Когато нашият C++ компилатор генерира код, той използва | Проблема е, че с добавянето на нова член-данна, ние сме променили размера на обектите от тип Widget и Label. Какво значение има това? Когато нашият C++ компилатор генерира код, той използва 'отмествания' за да достъпва данните в обектите. | ||
Ето един доста опростен пример за това как горните | Ето един доста опростен пример за това как горните POD обекти може да са разположени в паметта. | ||
{| | {| | ||
| '''Разположение на Label в WidgetLib 1.0''' | |'''Разположение на Label в WidgetLib 1.0''' | ||
| '''Разположение на Label в WidgetLib 1.1''' | |'''Разположение на Label в WidgetLib 1.1''' | ||
|- | |- | ||
| m_geometry <отстъп 0> | |m_geometry <отстъп 0&gt; | ||
| m_geometry <offset 0> | |m_geometry <offset 0&gt; | ||
|- | |- | ||
| | |————— | ||
| m_stylesheet <отстъп 1> | |m_stylesheet <отстъп 1&gt; | ||
|- | |- | ||
| m_text <отстъп 1> | |m_text <отстъп 1&gt; | ||
| | |—————— | ||
|- | |- | ||
| | |————— | ||
| m_text <отстъп 2> | |m_text <отстъп 2&gt; | ||
|} | |} | ||
В !WidgetLib 1.0, ''m_text'' на Label е била на (логически) отстъп 1. Кодът генериран от компилатора в приложението за метода | В !WidgetLib 1.0, ''m_text'' на Label е била на (логически) отстъп 1. Кодът генериран от компилатора в приложението за метода <code&gt;Label::text()</code&gt; търси текста на отстъп 1 от началото на Label обекта. В WidgetLib 1.1, член-данната ''text'' на Label е преместена на (логически) отстъп 2! Тъй като приложението не е било прекомпилирано, то продължава да мисли, че <code&gt;text&lt;/code&gt; е на отстъп 1 и така достъпва променливата <code&gt;stylesheet&lt;/code&gt;! | ||
А тази ситуация променя ли се, ако | === За тези, завършили C++ 101 :<s>) | ||
<br />Сигурен съм, че в този момент има няколко от вас, които се чудят защо пресмятането на отстъпа на <code&gt;Label::text()</code&gt; стана в CuteApp, а не в WidgetLib. Отговорът е, че кода за <code&gt;Label::text()</code&gt; беше дефиниран в хедър файла и компилатора го направи "вмъкната функция&quot;:http://en.wikipedia.org/wiki/Inline_function. | |||
<br />А тази ситуация променя ли се, ако <code&gt;Label::text()</code&gt; не беше вмъкнат? Да кажем, че <code&gt;Label::text()</code&gt; е беше преместен в сорс файла? Ами, не. C++ компилатора разчита на размера на обектите да бъде еднакъв в по време на компилация и по време на изпълнение. Например, пълненето/изпразването на стека</s> ако създадете обект от тип Label в стека, генерираният от компилатора код ще задели в стека място, базирайки се на размера на Label по време на компилация. Тъй като размера на Label е различен по време на изпълнение в WidgetLib 1.1, конструкторът на Label ще презапише съществуващите данни в стека, което ще доведе до прекратяване на програмата. === | |||
==Никога не променяйте размера на експортиран C++ клас== | == Никога не променяйте размера на експортиран C++ клас == | ||
В заключение, никога не променяйте размера или подредбата(не размествайте данните) на ''експортирани'' (т.е видими за потребителя) C++ класове веднъж щом вашата библиотека е пусната за използване. C++ компилатора генерира код като предполага, че размерът и подредбата на данните в класа ''не се променя след'' като приложението е било компилирано. | В заключение, никога не променяйте размера или подредбата(не размествайте данните) на ''експортирани'' (т.е видими за потребителя) C++ класове веднъж щом вашата библиотека е пусната за използване. C++ компилатора генерира код като предполага, че размерът и подредбата на данните в класа ''не се променя след'' като приложението е било компилирано. | ||
Line 58: | Line 64: | ||
Значи, как можем да добавим нови възможности без да променим размера на обекта? | Значи, как можем да добавим нови възможности без да променим размера на обекта? | ||
==d-pointer== | == d-pointer == | ||
Номера да запазите константен размера на всички публични класове в библиотеката като пазите само един указател. Този указател сочи към частна/вътрешна структура, която съдържа данните. Размерът на тази вътрешна структура може да се свива или расте без да има странични ефекти върху приложението, защото указателят се достъпва само в библиотеката и от гледна точка на приложението размера на обекта никога не се променя | Номера да запазите константен размера на всички публични класове в библиотеката като пазите само един указател. Този указател сочи към частна/вътрешна структура, която съдържа данните. Размерът на тази вътрешна структура може да се свива или расте без да има странични ефекти върху приложението, защото указателят се достъпва само в библиотеката и от гледна точка на приложението размера на обекта никога не се променя - той е винаги размера да указателя. Този указател се нарича ''d-pointer''. | ||
Духът на този подход е показан в следния код: | Духът на този подход е показан в следния код: | ||
<code><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 /></code> | |||
<br />С горната структура, CuteApp никога недостъпа d-pointer директно. И тъй като ''d-pointer'' се достъпва само от WidgetLib и WidgetLib се прекомпилира за всяка версия, ''Private'' класа може свободно да се променя, без това да има ефект върху CoolApp. | |||
<br />h2. Други ползи от d-pointer | |||
<br />Не всичко е заради бинарната съвместимост. От d-pointer има и други ползи:<br />''' Скрива имплементационните детайли - Можем да предоставяме WidgetLib само като хедър файлове и бинаред код. .cpp файловете могат да са затворен код.<br />* Хедър файла е изчистен от имплементационни детайли и може да служи като справка за API-то на класа.<br />* Тъй като хедър файловете, необходими за имплементацията са преместени от хедъра в сорс код файла, времето за компилация е доста по-малко. | |||
Вярно е, че горните ползи изглеждат незначително, но реалната причина да се използват d-pointer-и в Qt е бинарната съвместимост и факта, че Qt е започнала като библиотека със затворен код. | |||
== q-pointer == | |||
До тук, ние разгледахме само d-pointer-и като структура от данни в стил C. В действителност, обаче те съдържат частни методи (помощни функции). Например, <code&gt;LabelPrivate&lt;/code&gt; може да има <code&gt;getLinkTargetFromPoint()</code&gt; помощна функция, която трябва да взимма линк, когато мишката е натисната. В много случаи, тези помощни функции изискват достъп до публични класове, т.е някои функции от Label или от базовия клас Widget. Например, помощен метод, <code&gt;setTextAndUpdateWidget()</code&gt;, може да иска да извика <code&gt;Widget::update()</code&gt;, който е публичен метод за заявка за прерисуване на Widget. Така че, <code&gt;WidgetPrivate&lt;/code&gt; пази указател към публичният клас, който се нарича q-pointer. Като модифицираме горния код, за да ползва q-pointer, получаваме: | |||
<code><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 /></code> | |||
<br />h2. По-нататъшна оптимизация | |||
<br />В горният код, създаването на един Label води до заделянето на памет за <code&gt;LabelPrivate&lt;/code&gt; и <code&gt;WidgetPrivate&lt;/code&gt;. Ако приложим тази стратегия за Qt, ситуацията става доста лоша за класове като <code&gt;QListWidget&lt;/code&gt; - той има 6 нива на вложеност и това ще доведе до 6 заделяния на памет! | |||
<br />Това се решева като се въведе наследяване за нашите ''частни'' класове. Така като се създаде клас d-pointer се предава нагоре по йерархията. | |||
<br /><code><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> | |||
Виждате ли красотата? Сега когато създадем <code&gt;Label&lt;/code&gt; обект, той ще създаде <code&gt;LabelPrivate&lt;/code&gt; ( който е подклас на <code&gt;WidgetPrivate&lt;/code&gt;). Той подава конкретен ''d-pointer'' към защитеният конструктор на Widget! Сега, когато <code&gt;Label&lt;/code&gt; обекта е създаден, има само едно заделяне на памет. Label също има защитен конструктор, който може да се използва от клас, наследяващ <code&gt;Label&lt;/code&gt;, за да предостави свой собствен Private клас. | |||
== Превръщане на q-ptr и d-ptr в правилният тип (QPTR и DPTR) == | |||
Страничен ефек от оптимизацията, която направихме в предната стъпка е, че q-ptr и d-ptr са от тип <code&gt;Widget&lt;/code&gt; и <code&gt;WidgetPrivate&lt;/code&gt;. Това значи, че следното няма да работи: | |||
<code><br /> void Label::setText(const String &text) {<br /> // няма да работи! Тъй като d_ptr е от тип WidgetPrivate, въпреки че сочи към обекто то тип LabelPrivate<br /> d_ptr->text = text;<br /> }<br /></code> | |||
Следователно, когато достъпваме d-pointer в подклас, ние трябва да изпозлваме static_cast, за да превърнем указателя в правилният тип.<br /><code><br /> void Label::setText(const String &text) {<br /> LabelPrivate '''d = static_cast&lt;LabelPrivate'''>(d_ptr); // превръщане в правилният тип<br /> d->text = text;<br /> }<br /></code> | |||
Както може да видите, не е много приятно да имате static_cast навсякъде из кода. За това можем да си създадем макроси:<br /><code><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 /></code> | |||
== d-pointers в Qt == | |||
В Qt на практика всеки публичен клас използва d-pointer подхода. Единствените класове, в който той не е използван, са тези, за които е ясно, че никога няма да се наложи да имат допълнителни член-данни. Например, за класове като <code&gt;QPoint&lt;/code&gt; и <code&gt;QRect&lt;/code&gt;, не се очаква да се добавят нови членове, следователно данните се съхраняват директно в класа, вместо да се използва d-pointer. | |||
* В Qt, основният клас на всички Private обекти е <code&gt;QObjectPrivate&lt;/code&gt; | |||
* Макросите <code&gt;Q_D&lt;/code&gt; и <code&gt;Q_Q&lt;/code&gt; предоставят функциалността на дискутираните по-горе QPTR и DPTR. | |||
* Qt класовете имат макроса <code&gt;Q_DECLARE_PRIVATE&lt;/code&gt; в публичният клас. Макросът изглежда така:<br /><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> | |||
Идеята е, че <code&gt;QLabel&lt;/code&gt; предостая функция <code&gt;d_func()</code&gt;, която позволява достъп до частният си вътрешен клас. Самият метод е частен (поради това, че макроса е в частната секция на qlabel.h). <code&gt;d_func()</code&gt; обаче може да се извиква от '''приятелски класове''' на <code&gt;QLabel&lt;/code&gt;. Това основно е полезно за достъп до информация от Qt класове, които не могат да достъпват информация от <code&gt;QLabel&lt;/code&gt; през публичното API. Като странне пример, <code&gt;QLabel&lt;/code&gt; може да пази информация за това колко пъти потребителят е натискал на линк. Обаче няма публично API за достъп до тази информация. <code&gt;QStatistics&lt;/code&gt; е клас, който се нуждае от тази информация. Qt разратотчика ще добави <code&gt;QStatistics&lt;/code&gt; като приятел на <code&gt;QLabel&lt;/code&gt; и тогава <code&gt;QStatistics&lt;/code&gt; ще може да направи <code&gt;label->d_func()->linkClickCount&lt;/code&gt;. | |||
Друго предимство на <code&gt;d_func&lt;/code&gt; е, че налага правилно използване на ''const''. В константен метод на класа MyClass вие имате нужда от Q_D(const MyClass) и по този начин можете да извиквате само константни методи на MyClassPrivate. С "гол&quot; d_ptr може също да извиквате и не константни методи. | |||
Също така има и <code&gt;Q_DECLARE_PUBLIC&lt;/code&gt;, който се използва в Private класа, за деклариране на публичния клас. | |||
Revision as of 06:17, 24 February 2015
Български 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 класа, за деклариране на публичния клас.