Использование языка Pimpl в Qt, поиск лаконичного пути - PullRequest
0 голосов
/ 02 июля 2018

Моя проблема с Qt & pimpl на самом деле не проблема, а скорее просьба о совете передового опыта.

Итак: у нас довольно большой проект с большим количеством GUI и других классов Qt. Для правильной совместной работы требуется читабельность заголовков, поэтому сокращение времени компиляции также является вопросом регулярного рассмотрения.

Таким образом, у меня есть много классов, таких как:

class SomeAwesomeClass: public QWidget
{
    Q_OBJECT
public:
    /**/
    //interface goes here
    void doSomething();
    ...
private:
    struct SomeAwesomeClassImpl;
    QScopedPointer<SomeAwesomeClassImpl> impl;
}

Конечно, класс Pimpl находится в файле .cpp, работает нормально, например:

struct MonitorForm::MonitorFormImpl
{
    //lots of stuff
} 

Эта часть программного обеспечения должна быть кроссплатформенной (что не удивительно) и кросс-компилироваться без значительных усилий. Я знаю о Q_DECLARE_PRIVATE, Q_D и других макросах, они заставляют меня больше думать о Qt MOC, возможных различиях в версиях Qt (из-за унаследованного кода), но так или иначе, есть много строк кода, что-то вроде

impl->ui->component->doStuff();
//and
impl->mSomePrivateThing->doOtherStuff()
//and even
impl->ui->component->SetSomething(impl->mSomePrivateThing->getValue());

Псевдокод, приведенный выше, является значительно упрощенной версией реального, но большинство из нас с этим согласны. Но некоторые коллеги настаивают, что довольно сложно писать и читать все эти длинные строки, особенно когда impl->ui->mSomething-> повторяется слишком часто. Мнение гласит, что Qt marcos также добавляет визуальный обман в ситуацию. Seversl #define может помочь, но это считается плохой практикой.

Короче говоря, исходя из вашего опыта, есть ли способ сделать использование прыщей более лаконичным? Может быть, это не так часто требуется, как кажется, например, в небиблиотечных классах? Может быть, цели его использования не равны, в зависимости от обстоятельств?

Как правильно его готовить?

Ответы [ 3 ]

0 голосов
/ 03 июля 2018

Введение

Я знаю о Q_DECLARE_PRIVATE, Q_D и других макросах

Вы знаете о них, но действительно ли вы использовали их и поняли их назначение и - по большей части - их неизбежность? Эти макросы не были добавлены, чтобы сделать материал многословным. Они там, потому что вы в конечном итоге нуждаетесь в них.

Нет никаких отличий в реализации Qt PIMPL между версиями Qt, но вы зависите от деталей реализации Qt при наследовании от QClassPrivate, если вы это сделаете. Макросы PIMPL не имеют ничего общего с moc. Вы можете использовать их в простом коде C ++, который вообще не использует классы Qt.

Увы, вы не можете избежать того, что хотите, до тех пор, пока вы реализуете PIMPL обычным способом (который также является способом Qt).

Указатель Pimpl против этого

Прежде всего, давайте заметим, что impl означает this, но язык позволяет вам пропустить использование this-> в большинстве случаев. Таким образом, это не слишком чуждо.

class MyClassNoPimpl {
  int foo;
public:
  void setFoo(int s) { this->foo = s; }
};

class MyClass {
  struct MyClassPrivate;
  QScopedPointer<MyClassPrivate> const d;
public:
  void setFoo(int s);
  ...
  virtual ~MyClass();
};

void MyClass::setFoo(int s) { d->foo = s; }

Требования наследования ...

Вещи становятся вообще диковинными, когда у вас есть наследство:

class MyDerived : public MyClass {
  class MyDerivedPrivate;
  QScopedPointer<MyDerivedPrivate> const d;
public:
  void SetBar(int s);
};

void MyDerived::setFooBar(int f, int b) {
  MyClass::d->foo = f;
  d->bar = b;
}

Вы захотите повторно использовать один d-указатель в базовом классе, но он будет иметь неправильный тип во всех производных классах. Таким образом, вы могли бы подумать о том, чтобы разыграть его - это еще более шаблонно! Вместо этого вы используете закрытую функцию, которая возвращает правильно приведенный d-указатель. Теперь вам нужно получить как публичные, так и приватные классы, и вам нужны приватные заголовки для приватных классов, чтобы их могли использовать производные классы. О, и вам нужно передать указатель на производный pimpl в базовый класс - потому что это единственный способ, которым вы можете инициализировать d_ptr, сохраняя его константой, как и должно быть. Смотрите - Реализация PIMPL в Qt является многословной, потому что вам действительно нужно все это для написания безопасного, компонуемого, поддерживаемого кода. Обойти это невозможно.

MyClass1.h

class MyClass1 {
protected:
  struct Private;
  QScopedPointer<Private> const d_ptr;
  MyClass1(Private &); // a signature that won't clash with anything else
private:
  inline Private *d() { return (Private*)d_ptr; }
  inline const Private *d() const { return (const Private*)d_ptr; }
public:
  MyClass1();
  virtual ~MyClass1();
  void setFoo(int);
};

MyClass1_p.h

struct MyClass1::Private {
  int foo;
};

MyClass1.cpp

#include "MyClass1.h"
#include "MyClass1_p.h"

MyClass1::MyClass1(Private &p) : d_ptr(&p) {}

MyClass1::MyClass1() : d_ptr(new Private) {}    

MyClass1::~MyClass1() {} // compiler-generated

void MyClass1::setFoo(int f) {
  d()->foo = f;
}

MyClass2.h

#include "MyClass1.h"

class MyClass2 : public MyClass1 {
protected:
  struct Private;
private:
  inline Private *d() { return (Private*)d_ptr; }
  inline const Private *d() { return (const Private*)d_ptr; }
public:
  MyClass2();
  ~MyClass2() override; // Override ensures that the base had a virtual destructor.
                        // The virtual keyword is not used per DRY: override implies it.
  void setFooBar(int, int);
};

MyClass2_p.h

#include "MyClass1_p.h"

struct MyClass2::Private : MyClass1::Private {
  int bar;
};

MyClass2.cpp

MyClass2::MyClass2() : MyClass1(*new Private) {}

MyClass2::~MyClass2() {}

void MyClass2::setFooBar(int f, int b) {
  d()->foo = f;
  d()->bar = b;
}

Наследование, Qt way

Макросы Qt PIMPL заботятся о реализации функций d(). Ну, они реализуют d_func(), а затем вы используете макрос Q_D для получения локальной переменной, которая просто d. Переписав вышесказанное:

MyClass1.h

class MyClass1Private;
class MyClass1 {
  Q_DECLARE_PRIVATE(MyClass1)
protected:
  QScopedPointer<Private> d_ptr;
  MyClass1(MyClass1Private &);
public:
  MyClass1();
  virtual ~MyClass1();
  void setFoo(int);
};

MyClass1_p.h

struct MyClass1Private {
  int foo;
};

MyClass1.cpp

#include "MyClass1.h"
#include "MyClass1_p.h"

MyClass1::MyClass1(MyClass1Private &d) : d_ptr(*d) {}

MyClass1::MyClass1() : d_ptr(new MyClass1Private) {}  

MyClass1::MyClass1() {}

void MyClass1::setFoo(int f) {
  Q_D(MyClass1);
  d->foo = f;
}

MyClass2.h

#include "MyClass1.h"

class MyClass2Private;
class MyClass2 : public MyClass1 {
  Q_DECLARE_PRIVATE(MyClass2)
public:
  MyClass2();
  ~MyClass2() override;
  void setFooBar(int, int);
};

MyClass2_p.h

#include "MyClass1_p.h"

struct MyClass2Private : MyClass1Private {
  int bar;
};

MyClass2.cpp

MyClass2() : MyClass1(*new MyClass2Private) {}

MyClass2::~MyClass2() {}

void MyClass2::setFooBar(int f, int b) {
  Q_D(MyClass2);
  d->foo = f;
  d->bar = b;
}

Фабрики упрощают прыщ

Для иерархий классов, которые запечатаны (то есть там, где пользователь не извлекает), интерфейс может быть очищен от любых личных данных с помощью использования фабрик:

Интерфейсы

class MyClass1 {
public:
  static MyClass1 *make();
  virtual ~MyClass1() {}
  void setFoo(int);
};

class MyClass2 : public MyClass1 {
public:
  static MyClass2 *make();
  void setFooBar(int, int);
};

class MyClass3 : public MyClass2 {
public:
  static MyClass3 *make();
  void setFooBarBaz(int, int, int);
};

Реализация

template <class R, class C1, class C2, class ...Args, class ...Args2> 
R impl(C1 *c, R (C2::*m)(Args...args), Args2 &&...args) {
  return (*static_cast<C2*>(c).*m)(std::forward<Args2>(args)...);
}

struct MyClass1Impl {
  int foo;
};
struct  MyClass2Impl : MyClass1Impl {
  int bar;
};
struct MyClass3Impl : MyClass2Impl {
  int baz;
};

struct MyClass1X : MyClass1, MyClass1Impl {
   void setFoo(int f) { foo = f; }
};
struct MyClass2X : MyClass2, MyClass2Impl {
   void setFooBar(int f, int b) { foo = f; bar = b; }
};
struct MyClass3X : MyClass3, MyClass3Impl {
   void setFooBarBaz(int f, int b, int z) { foo = f; bar = b; baz = z;}
};

MyClass1 *MyClass1::make() { return new MyClass1X; }
MyClass2 *MyClass2::make() { return new MyClass2X; }
MyClass3 *MyClass3::make() { return new MyClass3X; }

void MyClass1::setFoo(int f) { impl(this, &MyClass1X::setFoo, f); }
void MyClass2::setFooBar(int f, int b) { impl(this, &MyClass2X::setFooBar, f, b); }
void MyClass3::setFooBarBaz(int f, int b, int z) { impl(this, &MyClass3X::setFooBarBaz, f, b, z); }

Это очень простой набросок, который необходимо доработать.

0 голосов
/ 03 июля 2018

@ KubaOber подробно рассказал о том, как работает pimpl и как его реализовать. Одна вещь, которую вы не рассмотрели, - это неизбежные макросы, упрощающие шаблон. Давайте посмотрим на возможную реализацию, заимствованную из моей собственной библиотеки ножей Swiss Army, которая явно основана на взятии Qt.

Во-первых, нам нужен базовый открытый интерфейс и базовая частная реализация с образцом. Наследование напрямую от реализации Qt бесполезно, если мы не используем Qt (и, кроме того, невероятно плохая идея), поэтому мы просто создадим легкий базовый класс для реализации (или d_ptr) и обратный указатель реализации на интерфейс (q_ptr).

#include <QScopedPointer> //this could just as easily be std::unique_ptr

class PublicBase; //note the forward declaration
class PrivateBase
{
public:
    //Constructs a new `PrivateBase` instance with qq as the back-pointer.
    explicit PrivateBase(PublicBase *qq);

    //We declare deleted all other constructors
    PrivateBase(const PrivateBase &) = delete;
    PrivateBase(PrivateBase &&) = delete;
    PrivateBase() = delete;

    //! Virtual destructor to prevent slicing.
    virtual ~PrivateBase() {}

    //...And delete assignment operators, too
    void operator =(const PrivateBase &) = delete;
    void operator =(PrivateBase &&) = delete;
protected:
    PublicBase *qe_ptr;
};

class PublicBase
{
public:
    //! The only functional constructor. Note that this takes a reference, i.e. it cannot be null.
    explicit PublicBase(PrivateBase &dd);

protected:
    QScopedPointer<PrivateBase> qed_ptr;
};


//...elsewhere
PrivateBase::PrivateBase(PublicBase *qq)
    : qe_ptr(qq)
{
}

PublicBase::PublicBase(PrivateBase &dd)
    : qed_ptr(&dd) //note that we take the address here to convert to a pointer
{
}

Теперь к макросам.

/* Use this as you would the Q_DECLARE_PUBLIC macro. */
#define QE_DECLARE_PUBLIC(Classname) \
    inline Classname *qe_q_func() noexcept { return static_cast<Classname *>(qe_ptr); } \
    inline const Classname* qe_cq_func() const noexcept { return static_cast<const Classname *>(qe_ptr); } \
    friend class Classname;

/* Use this as you would the Q_DECLARE_PRIVATE macro. */
#define QE_DECLARE_PRIVATE(Classname) \
    inline Classname##Private* qe_d_func() noexcept { return reinterpret_cast<Classname##Private *>(qed_ptr.data()); } \
    inline const Classname##Private* qe_cd_func() const noexcept { return reinterpret_cast<const Classname##Private *>(qed_ptr.data()); } \
    friend class Classname##Private;

Это довольно очевидно: они приводят сохраненный указатель к соответствующему производному типу. Макрос использует имя класса + "Private" для приведения к нужному типу. Это означает, что ваш закрытый класс ДОЛЖЕН следовать шаблону именования: InterfaceClass становится InterfaceClassPrivate. Чтобы разрешение области действия работало, они также должны находиться в одном и том же пространстве имен. Ваш частный класс не может быть членом вашего открытого класса.

И, наконец, средства доступа с изюминкой C ++ 11:

#define QE_DPTR         auto d = qe_d_func()
#define QE_CONST_DPTR   auto d = qe_cd_func()
#define QE_QPTR         auto q = qe_q_func()
#define QE_CONST_QPTR   auto q = qe_cq_func()

Отсутствие явного указания имени класса делает использование невероятно простым и менее жестким. Если этот класс будет переименован или функция перемещена на другой уровень в иерархии наследования, вам не нужно изменять оператор QE_CONST_DPTR.

SomeInterface::getFoo() const noexcept
{
    QE_CONST_DPTR;
    return d->foo;
}

станет:

SomeInterfaceInheritingFromSomeOtherInterface::getFoo() const noexcept
{
    QE_CONST_DPTR;
    return d->foo;
}
0 голосов
/ 02 июля 2018

Одной из целей PIMPL является отделение интерфейса от частной реализации. Примеры типа impl->ui->component->doStuff(); являются признаком того, что существует проблема с областью действия интерфейса. ИМХО, как правило, вы не должны видеть более одного глубокого звонка.

* 1004 Т.е. *

  • impl->doStuff(); ОК
  • impl->ui->doStuff(); Хммм, лучше избегайте этого.
  • impl->ui->component->... О, здесь все идет не так. Вызывающий должен знать слишком много деталей реализации. Это не цель PIMPL!

Возможно, вы захотите прочитать https://herbsutter.com/gotw/_100/, особенно раздел Какие части класса должны входить в объект impl

...