Как использовать идиому 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. Достаточно одной функции.