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

PIMPL означает P ointer to IMPL ementation. Реализация расшифровывается как "детали реализации": то, что пользователям класса не нужно беспокоиться.

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

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

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

скриншот диалога

Интерфейс на базе 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 линия.

Сравните это с не-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 и набор соглашений, которые необходимо соблюдать при использовании этой платформы. PIMPL Qt можно использовать во всех классах, даже если они не получены из QObject,

PIMPL должен быть размещен в куче. В идиоматическом 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 CoordinateDialog Интерфейс в вопросе.

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

  • PIMPL для класса 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) эквивалентно 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 код в место, где закрытый класс полностью определен. В нашем случае это означает, что 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 должен быть константным для не копируемых / не назначаемых классов, таких как QObject, Это может быть неконстантным при реализации копируемых классов.

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

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. В неконстантных методах он возвращает указатель на неконстантный интерфейс. Он также предоставляет интерфейс правильного типа в производных классах. Отсюда следует, что весь доступ к интерфейсу из 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 и Non-QObject

Идиома PIMPL позволяет реализовать копируемый, копируемый и перемещаемый конструируемый, назначаемый объект. Назначение выполняется через идиому копирования и замены, предотвращая дублирование кода. Конечно, указатель PIMPL не должен быть константным.

Напомним, что в 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. Одна функция будет делать.

Другие вопросы по тегам