Ответ 1
Введение
PIMPL - это частный класс, который содержит все данные, относящиеся к реализации родительского класса. Qt предоставляет структуру PIMPL и набор соглашений, которые необходимо соблюдать при использовании этой структуры. Qt PIMPL можно использовать во всех классах, даже тех, которые не получены из QObject
.
PIMPL необходимо выделить в кучу. В идиоматическом C++ мы не должны управлять этим хранилищем вручную, а использовать интеллектуальный указатель. Для этой цели работает либо QScopedPointer
либо std::unique_ptr
QScopedPointer
. Таким образом, минимальный интерфейс на основе 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
класс 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 он возвращает указатель на константу 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_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 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 должен быть const для непереписываемых/не назначаемых классов, таких как
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/non C++ 11. Для Qt 5 и C++ 11 это больше не требуется.
После принятия диалога мы фиксируем координаты и излучаем acceptedCoordinates
сигнал Coordinates. Вот почему нам нужен публичный указатель:
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. В не-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.
Общие готы
-
Заголовок интерфейса для данного класса должен быть первым заголовком, который должен быть включен в файл реализации. Это заставляет заголовок быть автономным и не зависит от деклараций, которые должны быть включены в реализацию. Если это не так, реализация не скомпилируется, что позволит вам исправить интерфейс, чтобы сделать его самодостаточным.
// 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 позволяет реализовать реализуемый, назначаемый объект с возможностью копирования, copy- и move-. Назначение выполняется с помощью идиомы copy- и-swap, предотвращая дублирование кода. Указатель 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
, которую мы также должны определить в интерфейсе. Обратите внимание, что это
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. Одна функция будет работать.