Pull to refresh

Что такое Pimpl по версии Qt, и с чем его едят!

Reading time12 min
Views38K

Вступление.



Часто в документации от Qt встречается термин Pimpl. Кроме того, те кто хоть немного копался в исходном коде Qt часто видел такие макросы как: Q_DECLARE_PRIVATE, Q_D. А также встречал так называемые приватные заголовочные файлы, название которых заканчивается на "_p.h".
В этой статье я попробую приоткрыть ширму за всей это структурой.

Pimpl, что это?


Pimpl — Pointer to private implementation. Это одно из названий паттерна программирования. Еще его называют чеширским котом — «Cheshire Cat» (это название мне больше нравится). В чем суть этого паттерна? Основная идея этого паттерна — это вынести все приватные члены класса и, в не которых случаях, функционала в приватный класс.
Отсюда название «чеширский кот» — видно только улыбку, а все остальное остается невидимым, но оно несомненно есть :-) Кто не помнит этого замечательного кота, может обратится к первоисточнику, к книге Льюиса Кэрролла «Алиса в стране чудес». Очень интересная книга, особенно если читать в оригинале.
Что это дает?

  1. Если иерархия классов «широкая» или «глубокая» получается более комплексная структура классов и тем самым повышается удобство «переиспользование — reuse» кода.
  2. Также это дает возможность спрятать платформо-зависимую реализацию от конечного пользователя.
  3. Одно из основных назначений этого паттерна — это предоставление механизма для реализации бинарной совместимости библиотеки при изменение ее реализации(достигается за счет того что вся реализация находится в приватном классе). Более подробно о бинарной совместимости и бинарном интерфейсе приложения (ABI) можно ознакомится здесь (Itanium C++ ABI) и здесь (an article about calling conversion). И основные правила о бинарной совместимости от KDE разработчиков Binary Compatibility Issues With C++ с кратким сборником того что нужно и чего нельзя делать для сохранения бинарной совместимости.
  4. Следующим достоинством является то что количество экспортируемых символов в классе становится меньше и скорость загрузки библиотеки увеличивается, ну и плюс меньший размер конечно.
  5. Увеличивается скорость сборки приложения (что очень актуально).
  6. Прячется вся ненужная реализация от клиента, в отличие от приватных методов, pimpl объявление и реализацию не видно вообще.
  7. Этот паттерн очень сильно облегчает реализацию механизма implicit-sharing (не буду переводить, чтоб не плодить лишней терминологии). Механизм при котором при копировании классов не происходит копирование данных, а копирование происходит лишь тогда, когда копии класса потребуется изменить эти данные. Implicit-sharing реализован во всех контейнерных классах Qt. Для его реализации используется реализация Pimpl под названием «shared D-pointers». Вообще это емкая тема и требует отдельной статьи.


Но ведь должны же быть и минусы, скрывать их не буду, выложу как есть:
  1. Каждый конструктор, деструктор вызывают выделение и освобождение памяти, что увеличивают время создания класса.
  2. Каждый метод, который содержит доступ к приватной реализации добавляет плюс одну экстра инструкцию.


Учитывая эти недостатки, по мнению автора, использование этого паттерна для классов, которые будут содержатся в коде в большом количестве и создаваться/удалятся в процессе жизнедеятельности программы нецелесообразно. Допустим примером такой реализации может служить загрузка и хранение неких данных, вот классы, которые реализуют эти данные лучше сделать как можно проще. А любое изменение в таких классах предполагает что изменяется протокол самой передачи этих данных. Но могут быть и другие примеры такого рода классов. Поэтому основное правило при использование любого паттерна распространяется и на этот тоже: использовать необходимо оптимально, там где это действительно необходимо.
То есть этот паттерн необходимо использовать в том случае, если вы пишете библиотеку или планируете вынести этот функционал в будущем в отдельную библиотеку. В других случаях, по личному мнению автора, использование такого подхода избыточно.

Как это реализовано в библиотеках Qt.


В Qt коде используется подход d-указателей. Смысл в том что объявляется класс XXXPrivate и переменная публичного класса в защищенной секции. В отдельном заголовочном файле или в .cpp файле уже пишется реализация приватного класса (в чем разница я объясню позже).
Иерархия классов идет как по публичным так и по приватным классам. Для этого объявление приватного класса обычно делается в отдельном .h файле, который называется так-же как публичный, но добавляется приставка _p: qclassname_p.h. И эти классы не устанавливаются вместе с библиотекой, а служат лишь для сборки библиотеки. Поэтому вы их не найдете в пути, куда установились библиотеки QT (Prefix-PATH).

В чем достоинства подхода d-указателей (d-pointers) от Qt?


Этот подход с первого взгляда может показаться немного запутанным, но я вас уверяю, что он в действительности очень простой и наглядный, и даже облегчает читабельность кода (это свойство я отношу к субъективным, поэтому спорно, все зависит от восприятия конкретного человека).
Достоинства:
  1. Простой и наглядный.
  2. Прямой доступ ко всей иерархии приватных классов (в действительности они не приватные :-), а защищенные).
  3. Возможность доступа к публичному классу из приватного.
  4. Возможность реализовать систему сигналов и слотов для приватного класса и спрятать их от внешнего использования с помощью 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 файл:
  1. TEMPLATE = lib
  2. HEADERS += myclass.h \
  3.   myclass_p.h \
  4.   myclassderived.h \
  5.   myclassderived_p.h
  6. SOURCES += myclass.cpp \
  7.   myclassderived.cpp


файл myclass.h:

  1. #ifndef MYCLASS_H
  2. #define MYCLASS_H
  3.  
  4. #include <QObject>
  5.  
  6. class MyClassPrivate;
  7. class MyClass : public QObject
  8. {
  9. Q_OBJECT
  10. public:
  11.   explicit MyClass(QObject *parent = 0);
  12.   int foo() const;
  13. signals:
  14.   void signal(int);
  15. protected:
  16.   MyClassPrivate * const d_ptr;
  17.   MyClass(MyClassPrivate &dd, QObject * parent);
  18. private:
  19.   Q_DECLARE_PRIVATE(MyClass);
  20. };
  21.  
  22. #endif // MYCLASS_H


файл myclass_p.h
  1. #ifndef MYCLASS_P_H
  2. #define MYCLASS_P_H
  3. #include "myclass.h"
  4.  
  5. class MyClassPrivate
  6. {
  7.   Q_DECLARE_PUBLIC(MyClass);
  8. public:
  9.   MyClassPrivate();
  10.   virtual ~MyClassPrivate();
  11.  
  12.   void foo();
  13.  
  14.   int i;
  15.   MyClass * q_ptr;
  16. };
  17.  
  18. #endif // MYCLASS_P_H


файл myclass.cpp
  1. #include "myclass.h"
  2. #include "myclass_p.h"
  3.  
  4.  
  5. MyClassPrivate::MyClassPrivate()
  6. {
  7.   i = 5;
  8. }
  9.  
  10. MyClassPrivate::~MyClassPrivate()
  11. {
  12.   //nothing to do
  13. }
  14.  
  15. void MyClassPrivate::foo()
  16. {
  17.   Q_Q(MyClass);
  18.   emit(q->signal(i));
  19. }
  20.  
  21.  
  22.  
  23. MyClass::MyClass(QObject *parent)
  24.   :QObject(parent)
  25.   ,d_ptr(new MyClassPrivate())
  26. {
  27.   Q_D(MyClass);
  28.   d->q_ptr = this;
  29. }
  30.  
  31. MyClass::MyClass(MyClassPrivate &dd, QObject * parent)
  32.   :QObject(parent)
  33.   ,d_ptr(&dd)
  34. {
  35.   Q_D(MyClass);
  36.   d->q_ptr = this;
  37. }
  38.  
  39.  
  40. int MyClass::foo() const
  41. {
  42.   Q_D(const MyClass);
  43.   return d->i;
  44. }


файл myclassderived.h
  1. #ifndef MYCLASSDERIVED_H
  2. #define MYCLASSDERIVED_H
  3. #include "myclass.h"
  4.  
  5. class MyClassDerivedPrivate;
  6. class MyClassDerived : public MyClass
  7. {
  8. Q_OBJECT
  9. public:
  10.   explicit MyClassDerived(QObject *parent = 0);
  11. signals:
  12.   void signal2(int);
  13. protected:
  14.   MyClassDerived(MyClassDerivedPrivate &dd, QObject * parent);
  15. private:
  16.   Q_DECLARE_PRIVATE(MyClassDerived);
  17. };
  18.  
  19. #endif // MYCLASSDERIVED_H


файл myclassderived_p.h
  1. #ifndef MYCLASSDERIVED_P_H
  2. #define MYCLASSDERIVED_P_H
  3.  
  4. #include "myclassderived.h"
  5. #include "myclass_p.h"
  6.  
  7. class MyClassDerivedPrivate: public MyClassPrivate
  8. {
  9.   Q_DECLARE_PUBLIC(MyClassDerived);
  10. public:
  11.   MyClassDerivedPrivate();
  12.   virtual ~MyClassDerivedPrivate();
  13.  
  14.   void foo2();
  15.   int j;
  16. };
  17.  
  18. #endif // MYCLASSDERIVED_P_H


файл myclassderived.cpp
  1. #include "myclassderived.h"
  2. #include "myclassderived_p.h"
  3.  
  4.  
  5.  
  6. MyClassDerivedPrivate::MyClassDerivedPrivate()
  7. {
  8.   j=6;
  9.   i=7;
  10. }
  11.  
  12. MyClassDerivedPrivate::~MyClassDerivedPrivate()
  13. {
  14.  
  15. }
  16.  
  17. void MyClassDerivedPrivate::foo2()
  18. {
  19.   Q_Q(MyClassDerived);
  20.   emit(q->signal2(j));
  21.   emit(q->signal(j));
  22. }
  23.  
  24. MyClassDerived::MyClassDerived(QObject *parent)
  25.   :MyClass(*new MyClassDerivedPrivate(), parent)
  26. {
  27. //Empty
  28. }
  29.  
  30. MyClassDerived::MyClassDerived(MyClassDerivedPrivate &dd, QObject * parent)
  31.     :MyClass(dd, parent)
  32. {
  33. //Empty
  34. }
Tags:
Hubs:
+44
Comments70

Articles