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