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

PIMPL означает Pointer к IMPLementation. Реализация означает "деталь реализации": то, что пользователям класса не нужно беспокоиться.

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

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

мотивация для использования интеллекту становится очевидной, когда мы что-то с полу-сложная реализация. Дальнейшая МОТИВАЦИЯ приведена в этот вопрос. Даже довольно простой класс должен вытащить много других заголовков в своем интерфейсе.

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, C++11 не нуждается в Q_PRIVATE_SLOT линии.

сравните это с интерфейсом non-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)

эти два интерфейса в точности эквивалентно что касается их публичного интерфейса. У них одинаковые сигналы, слоты и публичные методы.

1 ответов


введение

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

сутенер должен быть выделен в куче. В idiomatic c++ мы не должны управлять таким хранилищем вручную, а использовать интеллектуальный указатель. Либо QScopedPointer или std::unique_ptr работать для этой цели. Таким образом, минимальный интерфейс на основе 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 класс:

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

Читайте также:

Интерфейс

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

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

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

макрос Q_DECLARE_PRIVATE

на Q_DECLARE_PRIVATE макрос должен быть помещен в тег private раздел Объявления класса. Он принимает имя класса интерфейса в качестве параметра. Он объявляет две встроенные реализации d_func() вспомогательный метод. Этот метод возвращает указатель PIMPL с правильной константой. При использовании в методах const он возвращает указатель на const 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) is эквивалентно 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's 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-сгенерированный код в место, где частный класс полностью определен. В нашем случае, это означает, что должны конец с:

#include "moc_CoordinateDialog.cpp"

Gotchas

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

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

    // 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 ожидает имя класса интерфейса , не имя сутенера:

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

  • поскольку PIMPL является внутренней деталью реализации, ее размер недоступен на сайте, где используется интерфейс. Соблазн использовать размещение new и The Быстрый Pimpl идиома должна сопротивляться, поскольку она не обеспечивает преимущества для всего, кроме класса, который вообще не выделяет память.

Реализация

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

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

// 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 будет поймана компилятором. Как правило, лучше всего явно отключить функцию копирования с помощью 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 сигнал. Вот почему нам нужна публика. указатель:

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

для доступа к экземпляру интерфейса из non-const 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 должны манипулировать базовым классом интерфейса или излучать его сигналы. Эквивалент Q_DECLARE_PRIVATE макрос использовался для разрешения доступа к интеллекту из интерфейса.

макрос принимает имя класса, интерфейса в качестве параметра. Он объявляет две встроенные реализации q_func() вспомогательный метод. Этот метод возвращает указатель интерфейса с правильной константой. При использовании в методах 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.

Общие Gotchas

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

    // 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 и не-QObject копируемые классы

идиома PIMPL позволяет реализовать копируемый, копировать и перемещать конструктивный, назначаемый объект. Задание выполняется через скопировать и заменить идиома, предотвращения дублирования кода. Указатель 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 freestanding функция, которую мы должны определить в интерфейсе также. Обратите внимание, что это

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() {}

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