Вступление.
Часто в документации от Qt встречается термин Pimpl. Кроме того, те кто хоть немного копался в исходном коде Qt часто видел такие макросы как: Q_DECLARE_PRIVATE, Q_D. А также встречал так называемые приватные заголовочные файлы, название которых заканчивается на "_p.h".
В этой статье я попробую приоткрыть ширму за всей это структурой.
Pimpl, что это?
Pimpl — Pointer to private implementation. Это одно из названий паттерна программирования. Еще его называют чеширским котом — «Cheshire Cat» (это название мне больше нравится). В чем суть этого паттерна? Основная идея этого паттерна — это вынести все приватные члены класса и, в не которых случаях, функционала в приватный класс.
Отсюда название «чеширский кот» — видно только улыбку, а все остальное остается невидимым, но оно несомненно есть :-) Кто не помнит этого замечательного кота, может обратится к первоисточнику, к книге Льюиса Кэрролла «Алиса в стране чудес». Очень интересная книга, особенно если читать в оригинале.
Что это дает?
- Если иерархия классов «широкая» или «глубокая» получается более комплексная структура классов и тем самым повышается удобство «переиспользование — reuse» кода.
- Также это дает возможность спрятать платформо-зависимую реализацию от конечного пользователя.
- Одно из основных назначений этого паттерна — это предоставление механизма для реализации бинарной совместимости библиотеки при изменение ее реализации(достигается за счет того что вся реализация находится в приватном классе). Более подробно о бинарной совместимости и бинарном интерфейсе приложения (ABI) можно ознакомится здесь (Itanium C++ ABI) и здесь (an article about calling conversion). И основные правила о бинарной совместимости от KDE разработчиков Binary Compatibility Issues With C++ с кратким сборником того что нужно и чего нельзя делать для сохранения бинарной совместимости.
- Следующим достоинством является то что количество экспортируемых символов в классе становится меньше и скорость загрузки библиотеки увеличивается, ну и плюс меньший размер конечно.
- Увеличивается скорость сборки приложения (что очень актуально).
- Прячется вся ненужная реализация от клиента, в отличие от приватных методов, pimpl объявление и реализацию не видно вообще.
- Этот паттерн очень сильно облегчает реализацию механизма implicit-sharing (не буду переводить, чтоб не плодить лишней терминологии). Механизм при котором при копировании классов не происходит копирование данных, а копирование происходит лишь тогда, когда копии класса потребуется изменить эти данные. Implicit-sharing реализован во всех контейнерных классах Qt. Для его реализации используется реализация Pimpl под названием «shared D-pointers». Вообще это емкая тема и требует отдельной статьи.
Но ведь должны же быть и минусы, скрывать их не буду, выложу как есть:
- Каждый конструктор, деструктор вызывают выделение и освобождение памяти, что увеличивают время создания класса.
- Каждый метод, который содержит доступ к приватной реализации добавляет плюс одну экстра инструкцию.
Учитывая эти недостатки, по мнению автора, использование этого паттерна для классов, которые будут содержатся в коде в большом количестве и создаваться/удалятся в процессе жизнедеятельности программы нецелесообразно. Допустим примером такой реализации может служить загрузка и хранение неких данных, вот классы, которые реализуют эти данные лучше сделать как можно проще. А любое изменение в таких классах предполагает что изменяется протокол самой передачи этих данных. Но могут быть и другие примеры такого рода классов. Поэтому основное правило при использование любого паттерна распространяется и на этот тоже: использовать необходимо оптимально, там где это действительно необходимо.
То есть этот паттерн необходимо использовать в том случае, если вы пишете библиотеку или планируете вынести этот функционал в будущем в отдельную библиотеку. В других случаях, по личному мнению автора, использование такого подхода избыточно.
Как это реализовано в библиотеках Qt.
В Qt коде используется подход d-указателей. Смысл в том что объявляется класс XXXPrivate и переменная публичного класса в защищенной секции. В отдельном заголовочном файле или в .cpp файле уже пишется реализация приватного класса (в чем разница я объясню позже).
Иерархия классов идет как по публичным так и по приватным классам. Для этого объявление приватного класса обычно делается в отдельном .h файле, который называется так-же как публичный, но добавляется приставка _p: qclassname_p.h. И эти классы не устанавливаются вместе с библиотекой, а служат лишь для сборки библиотеки. Поэтому вы их не найдете в пути, куда установились библиотеки QT (Prefix-PATH).
В чем достоинства подхода d-указателей (d-pointers) от Qt?
Этот подход с первого взгляда может показаться немного запутанным, но я вас уверяю, что он в действительности очень простой и наглядный, и даже облегчает читабельность кода (это свойство я отношу к субъективным, поэтому спорно, все зависит от восприятия конкретного человека).
Достоинства:
- Простой и наглядный.
- Прямой доступ ко всей иерархии приватных классов (в действительности они не приватные :-), а защищенные).
- Возможность доступа к публичному классу из приватного.
- Возможность реализовать систему сигналов и слотов для приватного класса и спрятать их от внешнего использования с помощью Q_PRIVATE_SLOT макроса (тема для отдельной статьи).
Хочу такую же, но с перламутровыми пуговицами! (с) х/ф «Брильянтовая рука»
Если я вас убедил, что Pimpl — это хорошо, и вы хотите попробовать и посмотреть как это работает, тогда давайте я вас посвящу в реализацию Pimpl по версии Qt.
Что нужно сделать:
1. Сделать forward — объявление вашего приватного класса перед объявлением публичного класса:
class MyClassPrivate;
class MyClass: public QObject
{ ..............
2. Далее в первом классе иерархии в защищенной секции необходимо объявить переменную, ссылающуюся на приватный класс:
protected:
MyClassPrivate * const d_ptr;
Обратите внимание, что указатель константный, во избежании всяких там случайных нелепостей.
3. Также в защищенной секции (как первого класса иерархии так и всех его наследников) необходимо объявить конструктор, принимающий в качестве параметра приватный член класса. Это необходимо для того чтобы обеспечить возможность создание наследников приватного класса и использование их как приватных классов во всей иерархии:
protected:
MyClass(MyClassPrivate &&d, QObject *parent);
4. В приватной секции объявляем макрос доступа к d-указателю:
Q_DECLARE_PRIVATE(MyClass);
Вот теперь разберемся что он из себя представляет:
#define Q_DECLARE_PRIVATE(Class) \
inline Class##Private* d_func() { return reinterpret_cast<Class##Private *>(d_ptr); } \
inline const Class##Private* d_func() const { return reinterpret_cast<const Class##Private *>(d_ptr); } \
friend class Class##Private;
Как мы видим тут объявляется функция d_func() и d_func() const, с помощью которой мы получаем указатель на приватный класс и константный приватный класс соответственно. Причем получаем его уже приведенным к типу приватного класса этого объекта. Ну и объявляем наш приватный класс другом для публичного.
Также существует макрос Q_DECLARE_PRIVATE_D. Разница в том что в качестве второго параметра вы указываете переменную d-указатель, таким образом вместо d_ptr в качестве D-указателя может быть использована переменная с любым именем. Но название функции d_func остается неизменным.
Реализация макроса выглядит таким образом:
#define Q_DECLARE_PRIVATE_D(Dptr, Class) \
inline Class##Private* d_func() { return reinterpret_cast<Class##Private *>(Dptr); } \
inline const Class##Private* d_func() const { return reinterpret_cast<const Class##Private *>(Dptr); } \
friend class Class##Private;
5. Теперь необходимо объявить наш приватный класс в .cpp или _p.h файле. Если вы предполагаете дальнейшее наследование от вашего класса или собираетесь использовать приватные слоты, то необходимо вынести все в отдельный _p.h файл. Если же нет, то достаточно объявить приватный файл в .cpp файле. Также имейте ввиду, что в .pro файле .h файл должен идти до _p.h файла и до всех файлов, которые его включат в себя. Это можно взять вообще за правило, так как облегчает работу компилятору.
class MyClassPrivate
{
MyClassPrivate();
virtual ~MyClassPrivate();
int i;
}
Также рекомендую сделать деструктор виртуальным, если вы планируете строить иерархию приватных классов. Почему? Это тема отдельной статьи и таких статей уже написано достаточно, ну и конечно если не верите или не доверяете интернету, то обратитесь к Страуструпу, у него подробно излагается эта тема.
7. Реализация конструктора из защищенной секции будет выглядеть примерно таким образом:
MyClass::MyClass(MyClassPrivate &dd, QObject* parent)
:QObject(parent)
,d_ptr(&dd)
{ .....
Ну и обычный конструктор с таким объявлением (обратите внимание на ключевое слово explicit, если не знаете что это и зачем, поинтересуйтесь — это полезно):
explicit MyClass(QObject * parent);
Будет выглядеть таким образом:
MyClass::MyClass(QObject * parent)
:QObject(parent)
,d_ptr(new MyClassPrivate())
{........
В наследнике реализация такого конструктора будет выглядеть таким образом:
MyClassDerived::MyClassDerived(QObject * parent)
:MyClass(*new MyClassDerivedPrivate(),parent)
{........
Как вы видите соответствующий конструктор наследника передает экземпляр своего приватного класса во все базовые классы по цепочке иерархии наследования (так же устроено и в Qt иерархии классов; самым первым классом в иерархии приватных классов является QObjectData, который содержит в себе родителя, состояние объекта и другие базовые свойства).
8. Для доступа к экземпляру приватного класса из метода публичного класса существует макрос Q_D().Вот что он из себя представляет:
#define Q_D(Class) Class##Private * const d = d_func()
Как вы видите мы получаем константный указатель на наш приватный класс в виде переменной «d» (вот он D-указатель :-)!!! ).
int MyClass::foo() const
{
Q_D(const MyClass);
return d->i;
}
Обратите внимание, что в константных методах необходимо в Q_D макросе дописывать const перед именем класса, чтобы получить константный указатель на константный экземпляр приватного класса (если вас эта формулировка напугала или не до конца ясна, обратитесь к документации по «const», поверьте — это очень важно ).
9. Теперь погрузимся глубже. Разрешите представить еще одного зверя ;-): Q-указатель. Q-pointer (он же Q-указатель) — это тот же D-pointer (он же D-указатель), только с точностью наоборот. Он служит для доступа из методов приватного класса к экземпляру публичного (используется обычно в тех случаях, если логика тоже вынесена в приватный класс, или планируется это сделать в дальнейшем по цепочке иерархии).
Для его реализации необходимо в самом первом классе приватной иерархии объявить переменную-указатель на базовый класс:
class MyClassPrivate
{
public:
MyClassPrivate();
virtual ~MyClassPrivate();
int i;
MyClass *q_ptr;
}
И во всех классах иерархии объявить макрос Q_DECLARE_PUBLIC, в который планируется использовать Q-указатель.
class MyClassPrivate
{
Q_DECLARE_PUBLIC(MyClass);
public:
MyClassPrivate();
virtual ~MyClassPrivate();
int i;
MyClass *q_ptr;
}
Вот что из себя представляет макрос Q_DECLARE_PUBLIC:
#define Q_DECLARE_PUBLIC(Class) \
inline Class* q_func() { return static_cast<Class *>(q_ptr); } \
inline const Class* q_func() const { return static_cast<const Class *>(q_ptr); } \
friend class Class;
Как вы можете видеть, все тоже самое, как и в Q_DECLARE_PRIVATE, кроме названий. Ну и макроса для альтернативного названия q_ptr, наподобие Q_DECLARE_PRIVATE_D нет.
Важно: не забудьте позаботится во всех конструкторах первого класса публичной иерархии о инициализации переменной q_ptr:
MyClass::MyClass(QObject * parent)
:QObject(parent)
,d_ptr(new MyClassPrivate())
{
Q_D(MyClass);
d->q_ptr = this;
.......
}
10. Для доступа из методов приватного класса к публичному классу (например чтобы сделать вызов сигнала публичного класса ) существует макрос Q_Q, вот как он выглядит:
#define Q_Q(Class) Class * const q = q_func()
Логика та же что и для D-указателей, и те же правила. Ну и в коде он будет выглядеть таким образом:
void MyClassPrivate::foo()
{
Q_Q(MyClass);
q->foo();
emit(q->signal(i));
}
Заключение.
Имейте ввиду, что все эти макросы не являются частью публичного API, и могут быть в любой момент изменены. Но могу вас успокоить. Во первых это всё является настолько базовым фундаментом, что как минимум поменяется это к новой мажорной версии, но в этом случае все равно придется портировать приложение. А во вторых, очень многие крупные проекты используют эти макросы: например KDE. Ну а если вы являетесь убежденным параноиком и никому не доверяете, то можете объявить в своем глобальном файле похожие макросы, изменив их имя и использовать их в коде, тогда боятся точно нечего (кроме изменения поведения компиляторов по отношения к макросам :-), ибо настоящему параноику всегда есть чего боятся :-) ).
Также имейте ввиду что в моем примере я наследуюсь от QObject, который использует те же макросы для построения своей иерархии как публичных так и приватных классов. Но моя иерархия приватных классов ничего общего не имеет с приватной иерархией классов Qt. Они стоят в стороне и не мешают друг-другу. Так как я перекрыл переменную d_ptr в своем классе, и для всех наследников от моего класса d_ptr будет указателем на мою иерархию, а для QObject нет. Для него d_ptr будет Qt иерархией приватных классов (точнее указателем на QObjectPrivate).
Может возникнуть резонный вопрос, а почему бы наш приватный класс не унаследовать от QObjectPrivate. Ответ: можно, но во первых потеряется бинарная совместимость с библиотекой Qt(нужно будет иметь нашу библиотеку для каждой версии приватной реализации в Qt (она тоже меняется, QWidget точно) ).И вторым аргументом против является то, что для сборки нашей библиотеки потребуются приватные заголовочные файлы библиотеки Qt, которые находятся в папке src исходников, а не include установленных библиотек Qt. Да и трудно себе представить зачем это нужно(может кто-нибудь представит контр-аргументы, буду рад).
В дальнейшем я вам расскажу еще немного интересных вещей:
что такое QFlag, в чем его преимущество и что с ним едят.
Правила оформления кода в Qt-style.
Как реализовать Implicit Sharing и что такое Shared D-pointer.
Полезные макросы в QT.
Qt Creator снаружи и изнутри.
Как с минимальными усилиями написать приложение для 7 платформ.
И много всего другого интересного.
PS: Новые и интересные статьи я собираюсь выкладывать на своем сайте erudenko.com, правда на буржуйском языке. По возможности буду это переводить на русский и выкладывать здесь.
PPS: Приношу свои извинения за мой русский язык, возможные неверные обороты речи и ошибки (как орфографические так и синтаксические) — это не мой родной язык :-)
Фуууууух, ну вот вроде и все. В качестве дополнения простой пример реализации, а то я написал много букв, а на примере всегда наглядней:
.pro файл:
- TEMPLATE = lib
- HEADERS += myclass.h \
- myclass_p.h \
- myclassderived.h \
- myclassderived_p.h
- SOURCES += myclass.cpp \
- myclassderived.cpp
файл myclass.h:
- #ifndef MYCLASS_H
- #define MYCLASS_H
-
- #include <QObject>
-
- class MyClassPrivate;
- class MyClass : public QObject
- {
- Q_OBJECT
- public:
- explicit MyClass(QObject *parent = 0);
- int foo() const;
- signals:
- void signal(int);
- protected:
- MyClassPrivate * const d_ptr;
- MyClass(MyClassPrivate &dd, QObject * parent);
- private:
- Q_DECLARE_PRIVATE(MyClass);
- };
-
- #endif // MYCLASS_H
файл myclass_p.h
- #ifndef MYCLASS_P_H
- #define MYCLASS_P_H
- #include "myclass.h"
-
- class MyClassPrivate
- {
- Q_DECLARE_PUBLIC(MyClass);
- public:
- MyClassPrivate();
- virtual ~MyClassPrivate();
-
- void foo();
-
- int i;
- MyClass * q_ptr;
- };
-
- #endif // MYCLASS_P_H
файл myclass.cpp
- #include "myclass.h"
- #include "myclass_p.h"
-
-
- MyClassPrivate::MyClassPrivate()
- {
- i = 5;
- }
-
- MyClassPrivate::~MyClassPrivate()
- {
- //nothing to do
- }
-
- void MyClassPrivate::foo()
- {
- Q_Q(MyClass);
- emit(q->signal(i));
- }
-
-
-
- MyClass::MyClass(QObject *parent)
- :QObject(parent)
- ,d_ptr(new MyClassPrivate())
- {
- Q_D(MyClass);
- d->q_ptr = this;
- }
-
- MyClass::MyClass(MyClassPrivate &dd, QObject * parent)
- :QObject(parent)
- ,d_ptr(&dd)
- {
- Q_D(MyClass);
- d->q_ptr = this;
- }
-
-
- int MyClass::foo() const
- {
- Q_D(const MyClass);
- return d->i;
- }
файл myclassderived.h
- #ifndef MYCLASSDERIVED_H
- #define MYCLASSDERIVED_H
- #include "myclass.h"
-
- class MyClassDerivedPrivate;
- class MyClassDerived : public MyClass
- {
- Q_OBJECT
- public:
- explicit MyClassDerived(QObject *parent = 0);
- signals:
- void signal2(int);
- protected:
- MyClassDerived(MyClassDerivedPrivate &dd, QObject * parent);
- private:
- Q_DECLARE_PRIVATE(MyClassDerived);
- };
-
- #endif // MYCLASSDERIVED_H
файл myclassderived_p.h
- #ifndef MYCLASSDERIVED_P_H
- #define MYCLASSDERIVED_P_H
-
- #include "myclassderived.h"
- #include "myclass_p.h"
-
- class MyClassDerivedPrivate: public MyClassPrivate
- {
- Q_DECLARE_PUBLIC(MyClassDerived);
- public:
- MyClassDerivedPrivate();
- virtual ~MyClassDerivedPrivate();
-
- void foo2();
- int j;
- };
-
- #endif // MYCLASSDERIVED_P_H
файл myclassderived.cpp
- #include "myclassderived.h"
- #include "myclassderived_p.h"
-
-
-
- MyClassDerivedPrivate::MyClassDerivedPrivate()
- {
- j=6;
- i=7;
- }
-
- MyClassDerivedPrivate::~MyClassDerivedPrivate()
- {
-
- }
-
- void MyClassDerivedPrivate::foo2()
- {
- Q_Q(MyClassDerived);
- emit(q->signal2(j));
- emit(q->signal(j));
- }
-
- MyClassDerived::MyClassDerived(QObject *parent)
- :MyClass(*new MyClassDerivedPrivate(), parent)
- {
- //Empty
- }
-
- MyClassDerived::MyClassDerived(MyClassDerivedPrivate &dd, QObject * parent)
- :MyClass(dd, parent)
- {
- //Empty
- }