Введение
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() {}
Смотрите также:
Интерфейс
Теперь мы объясним в этом вопросе CoordinateDialog интерфейс на основе PIMPL.
Qt предоставляет несколько макросов и помощников по реализации, которые уменьшают утомительную работу PIMPL. Реализация ожидает, что мы будем следовать этим правилам:
- PIMPL для класса
Foo называется FooPrivate.
- PIMPL объявляется вперед вместе с объявлением класса
Foo в интерфейсном (заголовочном) файле.
Макрос Q_DECLARE_PRIVATE
Макрос Q_DECLARE_PRIVATE должен быть помещен в раздел private объявления класса. В качестве параметра он принимает имя класса интерфейса. Он объявляет две встроенные реализации вспомогательного метода d_func(). Этот метод возвращает указатель PIMPL с правильной константой. При использовании в методах const он возвращает указатель на const PIMPL. В неконстантных методах он возвращает указатель на неконстантный 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_PRIVATE(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"
Попался
Все макросы 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 является внутренней деталью реализации, его размер недоступен на сайте, где используется интерфейс. Следует противостоять соблазну использовать новое размещение и идиому 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 / не-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 в методе interface, мы можем использовать макрос Q_D, передав ему имя класса интерфейса.
void Class::foo() /* non-const */ {
Q_D(Class); /* needs a semicolon! */
// expands to
ClassPrivate * const d = d_func();
...
Чтобы получить доступ к PIMPL в методе константного интерфейса, нам нужно добавить к имени класса ключевое слово 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.
Общие проблемы
Заголовок interface для данного класса должен быть первым заголовком, включенным в файл реализации. Это заставляет заголовок быть самодостаточным и не зависеть от объявлений, которые случайно включены в реализацию. Если это не так, реализация не будет скомпилирована, что позволит вам исправить интерфейс, чтобы сделать его самодостаточным.
// 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, конечно, не должен быть константным.
В C ++ 11 нам необходимо соблюдать Правило четырех и предоставлять все из следующие: конструктор копирования, конструктор перемещения, оператор присваивания и деструктор. И, конечно же, автономная функция swap для реализации всего этого †.
Проиллюстрируем это на довольно бесполезном, но тем не менее правильном примере.
Интерфейс
// Integer.h
#include <algorithm>
#include <QScopedPointer>
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
#include "Integer.h"
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. Подойдет одна функция.
person
Kuba hasn't forgotten Monica
schedule
11.08.2014