D-Pointer/bg

From Qt Wiki
Jump to navigation Jump to search



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

[toc align_right="yes" depth="3"]

Какво е 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 трябва да може да се стартира.

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

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

<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&gt; m_geometry <offset 0&gt;
————— m_stylesheet <отстъп 1&gt;
m_text <отстъп 1&gt; ——————
————— m_text <отстъп 2&gt;

В !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 :)
Сигурен съм, че в този момент има няколко от вас, които се чудят защо пресмятането на отстъпа на <code&gt;Label::text()</code&gt; стана в CuteApp, а не в WidgetLib. Отговорът е, че кода за <code&gt;Label::text()</code&gt; беше дефиниран в хедър файла и компилатора го направи "вмъкната функция&quot;:http://en.wikipedia.org/wiki/Inline_function.
А тази ситуация променя ли се, ако <code&gt;Label::text()</code&gt; не беше вмъкнат? Да кажем, че <code&gt;Label::text()</code&gt; е беше преместен в сорс файла? Ами, не. C++ компилатора разчита на размера на обектите да бъде еднакъв в по време на компилация и по време на изпълнение. Например, пълненето/изпразването на стека
ако създадете обект от тип Label в стека, генерираният от компилатора код ще задели в стека място, базирайки се на размера на Label по време на компилация. Тъй като размера на Label е различен по време на изпълнение в WidgetLib 1.1, конструкторът на Label ще презапише съществуващите данни в стека, което ще доведе до прекратяване на програмата. ===

Никога не променяйте размера на експортиран 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 &quot;widget_p.h&amp;quot;<br /> Widget::Widget()<br /> : d_ptr(new WidgetPrivate) // създаване на частните данни {<br /> }
<br /> Rect Widget::geoemtry() const {<br /> // d-ptr се достъпва само в кода на библиотеката<br /> return d_ptr-&gt;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-&gt;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&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, получаваме:

<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 &quot;widget_p.h&amp;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-&gt;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-&gt;text;<br /> }<br />


h2. По-нататъшна оптимизация
В горният код, създаването на един Label води до заделянето на памет за <code&gt;LabelPrivate&lt;/code&gt; и <code&gt;WidgetPrivate&lt;/code&gt;. Ако приложим тази стратегия за Qt, ситуацията става доста лоша за класове като <code&gt;QListWidget&lt;/code&gt; - той има 6 нива на вложеност и това ще доведе до 6 заделяния на памет!
Това се решева като се въведе наследяване за нашите частни класове. Така като се създаде клас d-pointer се предава нагоре по йерархията.


<br /> /''' widget.h */<br /> class Widget {<br /> public:<br /> Widget();<br /> <br /> protected:<br /> // само подкласовете могат да достъпват долният код<br /> Widget(WidgetPrivate &amp;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 &amp;d)<br /> : d_ptr(&amp;d) {<br /> }
<br /> /''' label.h '''/<br /> class Label : public Widget {<br /> public:<br /> Label();<br /> <br /> protected:<br /> Label(LabelPrivate &amp;d); // позволява на подкалса Label да предаде своят Private<br /> // забележете, че Label няма d_ptr! Просто използва този на Widget.<br /> };
<br /> /''' label.cpp */<br /> #include &quot;widget_p.h&amp;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 &amp;d)<br /> : Widget(d) {<br /> }<br />

Виждате ли красотата? Сега когато създадем <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;. Това значи, че следното няма да работи:

<br /> void Label::setText(const String &amp;text) {<br /> // няма да работи! Тъй като d_ptr е от тип WidgetPrivate, въпреки че сочи към обекто то тип LabelPrivate<br /> d_ptr-&gt;text = text;<br /> }<br />

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

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

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

<br /> // global.h (macros)<br /> #define DPTR (Class) Class##Private '''d = static_cast&amp;lt;Class##Private'''&gt;(d_ptr)<br /> #define QPTR (Class) Class '''q = static_cast&amp;lt;Class'''&gt;(q_ptr)

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

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

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 /> // 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&amp;lt;Class##Private '''&gt;(qGetPtrHelper(d_ptr)); }  inline const Class##Private''' d_func() const {  return reinterpret_cast&amp;lt;const Class##Private *&gt;(qGetPtrHelper(d_ptr)); }  friend class Class##Private;<br />
    

Идеята е, че <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 класа, за деклариране на публичния клас.