Pimpl. D-pointer. Или чеширский кот по версии Qt.
Вступление.

Qt Logo
Часто в документации от Qt встречается термин Pimpl. Кроме того, те кто хоть немного копался в исходном коде Qt часто видел такие макросы как: Q_DECLARE_PRIVATE, Q_D. А также встречал так называемые приватные заголовочные файлы, название которых заканчивается на “_p.h”.
В этой статье я попробую приоткрыть ширму за всей это структурой.
Pimpl, что это?
Pimpl – Private implementation. Это одно из названий паттерна программирования. Еще его называют чеширским котом(это название мне больше нравится). В чем суть этого паттерна? Основная идея этого паттерна – это вынести все приватные члены класса и , в не которых случаях, функционала в приватный класс.
Отсюда название “чеширский кот” – видно только улыбку, а все остальное остается невидимым, но оно несомненно есть
Кто не помнит этого замечательного кота, может обратится к первоисточнику, к книге Льюиса Кэрролла «Алиса в стране чудес». Очень интересная книга, особенно если читать в оригинале.
Что это дает?
- Если иерархия классов “широкая” или “глубокая” получается более комплексная структура классов и тем самым повышается удобство “переиспользование – 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 &dd, QObject *parent);
4. В приватной секции объявляем макрос доступа к d-указателю:
Q_DECLARE_PRIVATE(MyClass);
Вот теперь разберемся что он из себя представляет:
#define Q_DECLARE_PRIVATE(Class) \
inline Class##Private* d_func() { return reinterpret_cast(d_ptr); } \
inline const Class##Private* d_func() const { return reinterpret_cast(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(Dptr); } \
inline const Class##Private* d_func() const { return reinterpret_cast(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(q_ptr); } \
inline const Class* q_func() const { return static_cast(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 платформ.
И много всего другого интересного.
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
}
Unique visitors to post: 213


protected:
MyClass(MyClassPrivate &&d, QObject *parent);
4. В приватной секции объяв….
исправьте на MyClassPrivate &dd.
За информацию мега-спасибо.
Это нагло, на об оформлении кода прошу поторопиться)
Да спасибо за исправлние. Поправил.
Троли вроде не собираются эту парадигму менять в ближайшем будущем
А к чему спешка то такая ?
Потому что я, делая небольшую программу, копирую фичи программирования от Qt. Оформление кода в том числе. Но я его не полностью разобрал. В частности комменты еще не затрагивал. А видя отличное изложение, вот и наглею)
Это опять я) Скажите, а кто удаляет созданный нами d_ptr?
опс, про это забыл, в деструкторе первого в дереве объекта нужно его удалить. Спасибо за внимательность, позже поправлю …
> И много всего другого интересного.
С нетерпением все еще ждем
Спасибо за русскоязычную статью!