Как использовать идиому Qt PIMPL?
PIMPL означает Pointer к IMPLementation. Реализация означает "деталь реализации": то, что пользователям класса не нужно беспокоиться.
собственные реализации класса Qt четко отделяют интерфейсы от реализаций посредством использования идиомы PIMPL. Тем не менее, механизмы, предоставляемые Qt, недокументированы. Как ими пользоваться?
Я хотел бы, чтобы это был канонический вопрос о том, "как я сутенер " в Qt. Ответы должны быть мотивированы простым диалоговым интерфейсом ввода координат, показанным ниже.
мотивация для использования интеллекту становится очевидной, когда мы что-то с полу-сложная реализация. Дальнейшая МОТИВАЦИЯ приведена в этот вопрос. Даже довольно простой класс должен вытащить много других заголовков в своем интерфейсе.

интерфейс на основе 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. Достаточно одной функции.