Подтвердить что ты не робот

Как использовать идиому Qt PIMPL?

PIMPL означает P на IMPL ementation. Реализация означает "деталь реализации": что-то, чего не должны беспокоить пользователи класса.

Qt собственных реализаций класса четко отделяет интерфейсы от реализаций с помощью идиомы PIMPL. Тем не менее, механизмы, предоставляемые Qt, недокументированы. Как их использовать?

Мне бы хотелось, чтобы это был канонический вопрос о том, "как сделать PIMPL" в Qt. Ответы должны быть мотивированы простым диалоговым интерфейсом с координатами ввода, показанным ниже.

Мотивация использования PIMPL становится очевидной, когда у нас есть что-либо с полукомплексной реализацией. Дальнейшая мотивация дается в этом вопросе. Даже довольно простой класс должен задействовать множество других заголовков в своем интерфейсе.

dialog screenshot

Интерфейс на основе PIMPL довольно чистый и читаемый.

// CoordinateDialog.h
#include <QDialog>
#include <QVector3D>

class CoordinateDialogPrivate;
class CoordinateDialog : public QDialog
{
  Q_OBJECT
  Q_DECLARE_PRIVATE(CoordinateDialog)
#if QT_VERSION <= QT_VERSION_CHECK(5,0,0)
  Q_PRIVATE_SLOT(d_func(), void onAccepted())
#endif
  QScopedPointer<CoordinateDialogPrivate> const d_ptr;
public:
  CoordinateDialog(QWidget * parent = 0, Qt::WindowFlags flags = 0);
  ~CoordinateDialog();
  QVector3D coordinates() const;
  Q_SIGNAL void acceptedCoordinates(const QVector3D &);
};
Q_DECLARE_METATYPE(QVector3D)

Интерфейс Qt 5, С++ 11 не нуждается в строке Q_PRIVATE_SLOT.

Сравните это с интерфейсом, отличным от PIMPL, который заталкивает детали реализации в частный раздел интерфейса. Обратите внимание на то, как должен быть добавлен другой код.

// CoordinateDialog.h
#include <QDialog>
#include <QVector3D>
#include <QFormLayout>
#include <QDoubleSpinBox>
#include <QDialogButtonBox>

class CoordinateDialog : public QDialog
{
  QFormLayout m_layout;
  QDoubleSpinBox m_x, m_y, m_z;
  QVector3D m_coordinates;
  QDialogButtonBox m_buttons;
  Q_SLOT void onAccepted();
public:
  CoordinateDialog(QWidget * parent = 0, Qt::WindowFlags flags = 0);
  QVector3D coordinates() const;
  Q_SIGNAL void acceptedCoordinates(const QVector3D &);
};
Q_DECLARE_METATYPE(QVector3D)

Эти два интерфейса в точности эквивалентны по отношению к их общему интерфейсу. Они имеют одинаковые сигналы, слоты и общедоступные методы.

4b9b3361

Ответ 1

Введение

PIMPL - это частный класс, который содержит все данные, относящиеся к реализации родительского класса. Qt предоставляет структуру PIMPL и набор соглашений, которые необходимо соблюдать при использовании этой структуры. Qt PIMPL можно использовать во всех классах, даже тех, которые не получены из QObject.

PIMPL необходимо выделить в кучу. В идиоматическом C++ мы не должны управлять этим хранилищем вручную, а использовать интеллектуальный указатель. Для этой цели работает либо QScopedPointer либо std::unique_ptr QScopedPointer. Таким образом, минимальный интерфейс на основе pimpl, не полученный из QObject, может выглядеть так:

// Foo.h
#include <QScopedPointer>
class FooPrivate; ///< The PIMPL class for Foo
class Foo {
  QScopedPointer<FooPrivate> const d_ptr;
public:
  Foo();
  ~Foo();
};

Объявление деструктора необходимо, так как деструктор указателя области действия должен уничтожить экземпляр PIMPL. Деструктор должен быть сгенерирован в файле реализации, где FooPrivate класс FooPrivate:

// Foo.cpp
class FooPrivate { };
Foo::Foo() : d_ptr(new FooPrivate) {}
Foo::~Foo() {}

Смотрите также:

Интерфейс

Теперь мы объясним интерфейс CoordinateDialog основе PIMPL.

Qt предоставляет несколько макросов и помощников по реализации, которые уменьшают нагрузку на PIMPL. Реализация предполагает, что мы соблюдаем следующие правила:

  • PIMPL для класса Foo называется FooPrivate.
  • PIMPL объявляется вперед по объявлению класса Foo в файле интерфейса (заголовка).

Макрос Q_DECLARE_PRIVATE

Макрос Q_DECLARE_PRIVATE должен быть помещен в private раздел объявления класса. В качестве параметра требуется имя класса интерфейса. Он объявляет две встроенные реализации вспомогательного метода d_func(). Этот метод возвращает указатель PIMPL с правильной константой. При использовании в методах const он возвращает указатель на константу PIMPL. В неконстантных методах он возвращает указатель на неконстантный PIMPL. Он также предоставляет pimpl правильного типа в производных классах. Из этого следует, что весь доступ к pimpl из реализации выполняется с использованием d_func() и ** не через d_ptr. Обычно мы будем использовать макрос Q_D, описанный в разделе "Реализация" ниже.

Макрос поставляется в двух вариантах:

Q_DECLARE_PRIVATE(Class)   // assumes that the PIMPL pointer is named d_ptr
Q_DECLARE_PRIVATE_D(Dptr, Class) // takes the PIMPL pointer name explicitly

В нашем случае Q_DECLARE_PRIAVATE(CoordinateDialog) эквивалентен Q_DECLARE_PRIVATE_D(d_ptr, CoordinateDialog).

Макрос Q_PRIVATE_SLOT

Этот макрос необходим только для совместимости с Qt 4 или при настройке на компиляторы C++ 11. Для кода Qt 5, C++ 11 это необязательно, поскольку мы можем подключить функторы к сигналам, и нет необходимости в явных частных слотах.

Иногда нам нужно, чтобы QObject имел частные слоты для внутреннего использования. Такие слоты будут загрязнять секретный раздел интерфейса. Поскольку информация о слотах применима только к генератору кода moc, мы можем вместо этого использовать макрос Q_PRIVATE_SLOT чтобы сообщить moc, что данный слот должен вызываться через указатель d_func(), а не через this.

Синтаксис, ожидаемый moc в Q_PRIVATE_SLOT, следующий:

Q_PRIVATE_SLOT(instance_pointer, method signature)

В нашем случае:

Q_PRIVATE_SLOT(d_func(), void onAccepted())

Это эффективно объявляет слот onAccepted в классе CoordinateDialog. Moc генерирует следующий код для вызова слота:

d_func()->onAccepted()

Сам макрос имеет пустое расширение - он предоставляет информацию только moc.

Таким образом, наш класс интерфейса расширяется следующим образом:

class CoordinateDialog : public QDialog
{
  Q_OBJECT /* We don't expand it here as it off-topic. */
  // Q_DECLARE_PRIVATE(CoordinateDialog)
  inline CoordinateDialogPrivate* d_func() { 
    return reinterpret_cast<CoordinateDialogPrivate *>(qGetPtrHelper(d_ptr));
  }
  inline const CoordinateDialogPrivate* d_func() const { 
    return reinterpret_cast<const CoordinateDialogPrivate *>(qGetPtrHelper(d_ptr));
  }
  friend class CoordinateDialogPrivate;
  // Q_PRIVATE_SLOT(d_func(), void onAccepted())
  // (empty)
  QScopedPointer<CoordinateDialogPrivate> const d_ptr;
public:
  [...]
};

При использовании этого макроса вы должны включить код, созданный moc, в место, где частный класс полностью определен. В нашем случае это означает, что файл CoordinateDialog.cpp должен заканчиваться:

#include "moc_CoordinateDialog.cpp"

Gotchas

  • Все макросы Q_, которые должны использоваться в объявлении класса, уже включают точку с запятой. После Q_ не нужны явные точки с запятой:

    // correct                       // verbose, has double semicolons
    class Foo : public QObject {     class Foo : public QObject {
      Q_OBJECT                         Q_OBJECT;
      Q_DECLARE_PRIVATE(...)           Q_DECLARE_PRIVATE(...);
      ...                              ...
    };                               };
    
  • PIMPL не должен быть частным классом внутри самого Foo:

    // correct                  // wrong
    class FooPrivate;           class Foo {
    class Foo {                   class FooPrivate;
      ...                         ...
    };                          };
    
  • Первый раздел после открытия скобки в объявлении класса по умолчанию является закрытым. Таким образом, следующие эквиваленты:

    // less wordy, preferred    // verbose
    class Foo {                 class Foo {              
      int privateMember;        private:
                                  int privateMember;
    };                          };
    
  • Q_DECLARE_PRIVATE ожидает имя класса интерфейса, а не имя PIMPL:

    // correct                  // wrong
    class Foo {                 class Foo {
      Q_DECLARE_PRIVATE(Foo)      Q_DECLARE_PRIVATE(FooPrivate)
      ...                         ...
    };                          };
    
  • Указатель PIMPL должен быть const для непереписываемых/не назначаемых классов, таких как QObject. Он может быть неконстантным при реализации классов с возможностью копирования.

  • Поскольку PIMPL является внутренней деталью реализации, его размер недоступен на сайте, где используется интерфейс. Искушение использовать новое место размещения и идиому Fast Pimpl следует избегать, поскольку оно не дает никаких преимуществ ни для чего, кроме класса, который вообще не выделяет память.

Реализация

PIMPL должен быть определен в файле реализации. Если он большой, его также можно определить в закрытом заголовке, обычно называемом foo_p.h для класса, интерфейс которого находится в foo.h

PIMPL, как минимум, является просто носителем данных основного класса. Ему нужен только конструктор и другие методы. В нашем случае также нужно сохранить указатель на основной класс, так как мы хотим испустить сигнал из основного класса. Таким образом:

// CordinateDialog.cpp
#include <QFormLayout>
#include <QDoubleSpinBox>
#include <QDialogButtonBox>

class CoordinateDialogPrivate {
  Q_DISABLE_COPY(CoordinateDialogPrivate)
  Q_DECLARE_PUBLIC(CoordinateDialog)
  CoordinateDialog * const q_ptr;
  QFormLayout layout;
  QDoubleSpinBox x, y, z;
  QDialogButtonBox buttons;
  QVector3D coordinates;
  void onAccepted();
  CoordinateDialogPrivate(CoordinateDialog*);
};

PIMPL не копируется. Поскольку мы используем не скопируемые элементы, любая попытка скопировать или назначить PIMPL будет захвачена компилятором. Как правило, лучше всего явно отключить функцию копирования, используя Q_DISABLE_COPY.

Макрос Q_DECLARE_PUBLIC работает аналогично Q_DECLARE_PRIVATE. Это описано ниже в этом разделе.

Мы передаем указатель на диалог в конструктор, позволяя нам инициализировать макет в диалоговом окне. Мы также подключаем принятый сигнал QDialog к внутреннему onAccepted.

CoordinateDialogPrivate::CoordinateDialogPrivate(CoordinateDialog * dialog) :
  q_ptr(dialog),
  layout(dialog),
  buttons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel)
{
  layout.addRow("X", &x);
  layout.addRow("Y", &y);
  layout.addRow("Z", &z);
  layout.addRow(&buttons);
  dialog->connect(&buttons, SIGNAL(accepted()), SLOT(accept()));
  dialog->connect(&buttons, SIGNAL(rejected()), SLOT(reject()));
#if QT_VERSION <= QT_VERSION_CHECK(5,0,0)
  this->connect(dialog, SIGNAL(accepted()), SLOT(onAccepted()));
#else
  QObject::connect(dialog, &QDialog::accepted, [this]{ onAccepted(); });
#endif
}

Метод onAccepted() PIMPL должен быть представлен как слот в проектах Qt 4/non C++ 11. Для Qt 5 и C++ 11 это больше не требуется.

После принятия диалога мы фиксируем координаты и излучаем acceptedCoordinates сигнал Coordinates. Вот почему нам нужен публичный указатель:

void CoordinateDialogPrivate::onAccepted() {
  Q_Q(CoordinateDialog);
  coordinates.setX(x.value());
  coordinates.setY(y.value());
  coordinates.setZ(z.value());
  emit q->acceptedCoordinates(coordinates);
}

Макрос Q_Q объявляет локальную переменную CoordinateDialog * const q. Это описано ниже в этом разделе.

Публичная часть реализации создает PIMPL и раскрывает ее свойства:

CoordinateDialog::CoordinateDialog(QWidget * parent, Qt::WindowFlags flags) :
  QDialog(parent, flags),
  d_ptr(new CoordinateDialogPrivate(this))
{}

QVector3D CoordinateDialog::coordinates() const {
  Q_D(const CoordinateDialog);
  return d->coordinates;
}

CoordinateDialog::~CoordinateDialog() {}

Макрос Q_D объявляет локальную переменную CoordinateDialogPrivate * const d. Это описано ниже.

Макрос Q_D

Чтобы получить доступ к PIMPL в методе интерфейса, мы можем использовать макрос Q_D, передавая ему имя класса интерфейса.

void Class::foo() /* non-const */ {
  Q_D(Class);    /* needs a semicolon! */
  // expands to
  ClassPrivate * const d = d_func();
  ...

Чтобы получить доступ к PIMPL в методе интерфейса const, нам нужно добавить имя класса с ключевым словом const:

void Class::bar() const {
  Q_D(const Class);
  // expands to
  const ClassPrivate * const d = d_func();
  ...

Макрос Q_Q

Чтобы получить доступ к экземпляру интерфейса из неконстантного метода PIMPL, мы можем использовать макрос Q_Q, передавая ему имя класса интерфейса.

void ClassPrivate::foo() /* non-const*/ {
  Q_Q(Class);   /* needs a semicolon! */
  // expands to
  Class * const q = q_func();
  ...

Чтобы получить доступ к экземпляру интерфейса в методе const PIMPL, мы добавляем имя класса с ключевым словом const же, как и для макроса Q_D:

void ClassPrivate::foo() const {
  Q_Q(const Class);   /* needs a semicolon! */
  // expands to
  const Class * const q = q_func();
  ...

Макрос Q_DECLARE_PUBLIC

Этот макрос является необязательным и используется для доступа к интерфейсу из PIMPL. Он обычно используется, если методы PIMPL должны обрабатывать базовый класс интерфейса или излучать его сигналы. Эквивалент Q_DECLARE_PRIVATE макрос используется для обеспечения доступа к Pimpl из интерфейса.

Макрос принимает имя класса интерфейса в качестве параметра. Он объявляет две встроенные реализации вспомогательного метода q_func(). Этот метод возвращает указатель интерфейса с правильной константой. При использовании в методах const он возвращает указатель на интерфейс const. В не-const-методах он возвращает указатель на неконстантный интерфейс. Он также обеспечивает интерфейс правильного типа в производных классах. Из этого следует, что весь доступ к интерфейсу из PIMPL должен выполняться с использованием q_func() и ** не через q_ptr. Обычно мы использовали макрос Q_Q, описанный выше.

Макрос ожидает, что указатель на интерфейс будет называться q_ptr. Существует не вариант с двумя аргументами этого макроса, который позволил бы выбрать другое имя для указателя интерфейса (как в случае с Q_DECLARE_PRIVATE).

Макрос расширяется следующим образом:

class CoordinateDialogPrivate {
  //Q_DECLARE_PUBLIC(CoordinateDialog)
  inline CoordinateDialog* q_func() {
    return static_cast<CoordinateDialog*>(q_ptr);
  }
  inline const CoordinateDialog* q_func() const {
    return static_cast<const CoordinateDialog*>(q_ptr);
  }
  friend class CoordinateDialog;
  //
  CoordinateDialog * const q_ptr;
  ...
};

Макрос Q_DISABLE_COPY

Этот макрос удаляет конструктор копирования и оператор присваивания. Он должен появиться в частном разделе PIMPL.

Общие готы

  • Заголовок интерфейса для данного класса должен быть первым заголовком, который должен быть включен в файл реализации. Это заставляет заголовок быть автономным и не зависит от деклараций, которые должны быть включены в реализацию. Если это не так, реализация не скомпилируется, что позволит вам исправить интерфейс, чтобы сделать его самодостаточным.

    // correct                   // error prone
    // Foo.cpp                   // Foo.cpp
    
    #include "Foo.h"             #include <SomethingElse>
    #include <SomethingElse>     #include "Foo.h"
                                 // Now "Foo.h" can depend on SomethingElse without
                                 // us being aware of the fact.
    
  • Макрос Q_DISABLE_COPY должен появиться в частном разделе PIMPL

    // correct                   // wrong
    // Foo.cpp                   // Foo.cpp
    
    class FooPrivate {           class FooPrivate {
      Q_DISABLE_COPY(FooPrivate) public:
      ...                          Q_DISABLE_COPY(FooPrivate)
    };                              ...
                                 };
    

Классы PIMPL и Non-QObject для копирования

Идиома PIMPL позволяет реализовать реализуемый, назначаемый объект с возможностью копирования, copy- и move-. Назначение выполняется с помощью идиомы copy- и-swap, предотвращая дублирование кода. Указатель PIMPL не должен быть const, конечно.

Напомним, что в C++ 11 нам нужно прислушаться к правилу четвертого и предоставить все следующее: конструктор копирования, конструктор перемещения, оператор присваивания и деструктор. И автономная функция swap чтобы реализовать все это, конечно.

Мы проиллюстрируем это, используя довольно бесполезный, но, тем не менее, правильный пример.

Интерфейс

// Integer.h
#include <algorithm>

class IntegerPrivate;
class Integer {
   Q_DECLARE_PRIVATE(Integer)
   QScopedPointer<IntegerPrivate> d_ptr;
public:
   Integer();
   Integer(int);
   Integer(const Integer & other);
   Integer(Integer && other);
   operator int&();
   operator int() const;
   Integer & operator=(Integer other);
   friend void swap(Integer& first, Integer& second) /* nothrow */;
   ~Integer();
};

Для производительности конструктор перемещения и оператор присваивания должны быть определены в файле интерфейса (заголовка). Им не нужно напрямую обращаться к PIMPL:

Integer::Integer(Integer && other) : Integer() {
   swap(*this, other);
}

Integer & Integer::operator=(Integer other) {
   swap(*this, other);
   return *this;
}

Все они используют функцию свободного swap, которую мы также должны определить в интерфейсе. Обратите внимание, что это

void swap(Integer& first, Integer& second) /* nothrow */ {
   using std::swap;
   swap(first.d_ptr, second.d_ptr);
}

Реализация

Это довольно просто. Нам не нужен доступ к интерфейсу из PIMPL, поэтому Q_DECLARE_PUBLIC и q_ptr отсутствуют.

// Integer.cpp
class IntegerPrivate {
public:
   int value;
   IntegerPrivate(int i) : value(i) {}
};

Integer::Integer() : d_ptr(new IntegerPrivate(0)) {}
Integer::Integer(int i) : d_ptr(new IntegerPrivate(i)) {}
Integer::Integer(const Integer &other) :
   d_ptr(new IntegerPrivate(other.d_func()->value)) {}
Integer::operator int&() { return d_func()->value; }
Integer::operator int() const { return d_func()->value; }
Integer::~Integer() {}

† За этим прекрасным ответ: Есть и другие требования, которые мы должны специализироваться std::swap для нашего типа, обеспечивают в классе swap вдоль бок свободно функция swap и т.д. Но это все лишнее: любое правильное использование swap будет через неквалифицированный звонок, и наша функция будет найдена через ADL. Одна функция будет работать.