Qt中的Pimpl习惯用法,寻找简洁的方式

时间:2018-07-02 18:13:55

标签: c++ qt pimpl-idiom

我对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可以提供帮助,但通常认为这是不好的做法。

简而言之,根据您的经验,有没有办法使pimpl的使用更加简洁?例如,在非图书馆类中,也许并不是真正需要的频率如此高?视情况而定,其使用目标可能不相等?

反正做饭的正确方法是什么?

3 个答案:

答案 0 :(得分:4)

简介

  

我了解Q_DECLARE_PRIVATE,Q_D和其他宏

您了解它们,但是您是否实际使用过它们并了解它们的目的,以及在大多数情况下它们的必然性?这些宏不是为了使内容变得冗长而添加的。它们在那里是因为您最终需要它们。

Qt版本之间的Qt PIMPL实现没有区别,但是如果您要继承QClassPrivate,则需要依靠Qt的实现细节。 PIMPL宏与moc无关。您可以在完全不使用任何Qt类的普通C ++代码中使用它们。

A,只要您以通常的方式(也是Qt方式)实现PIMPL,就不会逃避您想要的东西。

Pimpl-pointer vs this

首先,让我们观察一下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的同时保持常量不变的唯一方法。请参阅-Qt的PIMPL实现非常冗长,因为您确实需要所有这些才能编写安全,可组合且可维护的代码。没办法。

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方式继承

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); }

这是非常基本的草图,应进一步完善。

答案 1 :(得分:1)

@KubaOber很好地介绍了pimpl的工作方式和实现方式。您没有讨论的一件事是简化样板的不可避免的宏。让我们看一下从我自己的瑞士军刀库中借来的一个可能的实现,该实现显然基于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;
}

答案 2 :(得分:0)

PIMPL的一个目的是使接口与私有实现分离。诸如impl->ui->component->doStuff();之类的示例表明接口范围存在问题。恕我直言,您通常不应看到多个深层通话。

  • impl->doStuff(); OK
  • impl->ui->doStuff();嗯,最好避免这种情况。
  • impl->ui->component->...哦,这里出问题了。呼叫者需要了解太多实施细节。这不是PIMPL的目的!

您可能想阅读https://herbsutter.com/gotw/_100/,尤其是该类的哪些部分应放入impl对象中?