简体   繁体   English

在Qt中使用Pimpl成语,寻找简洁的方式

[英]Pimpl idiom usage in Qt, searching for laconic way

My problem with Qt & pimpl is not actually a problem, more a request for best-practice advice. 我对Qt&pimpl的问题实际上并不是问题,更多是对最佳实践建议的要求。

So: we've got quite a large project with lots of GUI and other Qt classes. 所以:我们有一个包含大量GUI和其他Qt类的大型项目。 Readability of headers is required for fine collaboration, reducing compilation time is also a matter of regular consideration. 标头的可读性是精细协作所必需的,减少编译时间也是经常考虑的问题。

Thus, there I have lots of classes like: 因此,我有很多类,如:

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

Of course, the Pimpl class is in the .cpp file, works fine, like: 当然,Pimpl类在.cpp文件中,工作正常,如:

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

This piece of software is supposed to be crossplatform (not a surprise) and cross-compiled without significant effort. 这个软件应该是跨平台(不是一个惊喜)和交叉编译而不需要很大的努力。 I know about Q_DECLARE_PRIVATE, Q_D and other macros, they make me think more about Qt MOC, possible differences in Qt versions (because of legacy code), but this way or another there are many lines of code contatinig something like 我知道关于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());

The pseudo-code above is a much simplified version of the real one, but most of us are fine with it. 上面的伪代码是真实代码的简化版本,但我们大多数人都很好。 But some colleagues insist, that it's rather bothering to write and read all those long lines, especially when impl->ui->mSomething-> is repeating too often. 但有些同事坚持认为,编写和阅读所有这些长线相当麻烦,特别是当impl->ui->mSomething->经常重复时。 The opinion states, that Qt marcos also add visual garbaging to the situation in the end. 该意见指出,Qt marcos最终还为这种情况增添了视觉效果。 Seversl #define 's could help, but those are considered generally bad practice. Seversl #define可以提供帮助,但这些通常被认为是不好的做法。

In short, based on your experience, is there a way to make pimpl usage more laconic? 总之,根据您的经验,有没有办法让pimpl使用更简洁? Maybe it isn't truly required as often as seems, in non-library classes for example? 例如,在非库类中,它可能并不是真正需要的吗? Maybe the goals of it's usage are not equal, depending on circumstances? 根据具体情况,它的使用目标可能并不相同?

What's the proper way to cook it, anyway? 无论如何,烹饪它的正确方法是什么?

Introduction 介绍

I know about Q_DECLARE_PRIVATE, Q_D and other macros 我知道Q_DECLARE_PRIVATE,Q_D和其他宏

You know about them, but have you actually used them and understand their purpose, and - for the most part - their inevitability? 你了解它们,但是你真的使用它们并了解它们的目的,并且 - 在大多数情况下 - 它们的必然性吗? Those macros weren't added to make stuff verbose. 那些宏没有被添加以使东西变得冗长。 They are there because you end up needing them. 他们在那里是因为你最终需要他们。

There are no differences in Qt PIMPL implementation between Qt versions, but you are depending on Qt's implementation details when you inherit from QClassPrivate , should you do so. Qt版本之间的Qt PIMPL实现没有区别,但是当你从QClassPrivate继承时,你依赖于Qt的实现细节,你是否应该这样做。 The PIMPL macros have nothing to do with moc. PIMPL宏与moc无关。 You can use them in plain C++ code that doesn't use any Qt classes at all. 您可以在不使用任何Qt类的普通C ++代码中使用它们。

Alas, there's no escaping what you want for as long as you implement the PIMPLs the usual way (which is also Qt way). 唉,只要你以通常的方式实现PIMPL(也是Qt方式),就没有逃避你想要的东西了。

Pimpl-pointer vs this Pimpl-pointer vs this

First of all, let's observe that impl stands for this , but the language lets you skip using this-> in most cases. 首先,让我们观察impl代表this ,但在大多数情况下,该语言允许您跳过使用this-> Thus, it's nothing too foreign. 因此,它没有什么太外国的。

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

Inheritance demands... 继承要求......

Things become generally outlandish when you have inheritance, though: 但是,当你有继承时,事情变得普遍变得古怪:

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

You'll want to re-use a single d-pointer in the base class, but it will have the wrong type in all derived classes. 您将要在基类中重用单个d指针,但在所有派生类中它将具有错误的类型。 Thus you might think of casting it - that's even more boilerplate! 因此,你可能会想到铸造它 - 这是更多的样板! Instead, you use a private function that returns a correctly-cast d-pointer. 相反,您使用一个返回正确转换的d指针的私有函数。 Now you need to derive both public and private classes, and you need private headers for the private classes, so that the derived classes can use them. 现在,您需要派生公共类和私有类,并且您需要私有类的私有标头,以便派生类可以使用它们。 Oh, and you need to pass the pointer to the derived pimpl to the base class - because that's the only way you can initialize the d_ptr while keeping it const, as it must be. 哦,你需要将指针传递给派生的pimpl到基类 - 因为这是你可以初始化d_ptr同时保持它的唯一方式,因为它必须是。 See - Qt's PIMPL implementation is verbose because you do actually need all of it to write safe, composable, maintainable code. 请参阅 - Qt的PIMPL实现是冗长的,因为您确实需要所有这些实现来编写安全,可组合,可维护的代码。 No way around it. 没办法解决它。

MyClass1.h 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 MyClass1_p.h

struct MyClass1::Private {
  int foo;
};

MyClass1.cpp 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 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 MyClass2_p.h

#include "MyClass1_p.h"

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

MyClass2.cpp MyClass2.cpp

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

MyClass2::~MyClass2() {}

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

Inheritance, Qt way 继承,Qt方式

Qt's PIMPL macros take care of implementing d() functions. Qt的PIMPL宏负责实现d()函数。 Well, they implement d_func() and then you use the Q_D macro to obtain a local variable that is simply d . 好吧,他们实现了d_func()然后你使用Q_D宏来获得一个简单的d局部变量。 Rewriting the above: 重写以上内容:

MyClass1.h 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 MyClass1_p.h

struct MyClass1Private {
  int foo;
};

MyClass1.cpp 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 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 MyClass2_p.h

#include "MyClass1_p.h"

struct MyClass2Private : MyClass1Private {
  int bar;
};

MyClass2.cpp MyClass2.cpp

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

MyClass2::~MyClass2() {}

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

Factories simplify pimpl 工厂简化了pimpl

For class hierarchies that are sealed (ie where the user doesn't derive), the interface can be sanitized from any private details whatsoever by the use of factories: 对于密封的类层次结构(即用户未派生的位置),可以通过使用工厂从任何私有详细信息中清除接口:

Interfaces 接口

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

Implementations 实现

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

This is very basic sketch that should be further refined. 这是非常基本的草图,应该进一步完善。

@KubaOber gave an excellent coverage of how pimpl works and how to implement it. @KubaOber非常详细地介绍了pimpl如何工作以及如何实现它。 One thing not covered that you discussed are the inevitable macros to simplify the boilerplate. 您讨论的一个未涵盖的事情是简化样板的不可避免的宏。 Let's take a look at a possible implementation, borrowed from my own Swiss Army knife library, which is clearly based on Qt's take. 让我们看一下从我自己的瑞士军刀库中借鉴的可能实现,这显然是基于Qt的观点。

Firstly, we need a base public interface and a base private implementation with the boilerplate. 首先,我们需要一个基本的公共接口和一个带有样板的基本私有实现。 Inheriting directly from Qt's implementation is useless if we aren't using Qt (and an incredibly bad idea besides), so we'll just create a lightweight base class for the implementation (or d_ptr ) and the implementation's back-pointer to the interface (the q_ptr ). 如果我们不使用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
{
}

Now to the macros. 现在到宏。

/* 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;

These are fairly self-explanatory: they cast the stored pointer to the appropriate derived type. 这些都是不言自明的:它们将存储的指针强制转换为适当的派生类型。 The macro leverages the class name + "Private" to cast to the right type. 宏利用类名+“Private”来强制转换为正确的类型。 This means your private class MUST follow the naming pattern: InterfaceClass becomes InterfaceClassPrivate . 这意味着您的私有类必须遵循命名模式: InterfaceClass成为InterfaceClassPrivate For scope resolution to work, they need to be in the same namespace, too. 要使范围解析起作用,它们也需要位于相同的命名空间中。 Your private class can't be a member of your public class. 您的私人课程不能成为您公共课程的成员。

And finally, the accessors, with a C++11 twist: 最后,访问者使用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()

Not having to explicitly specify the class name makes usage incredibly easy and less rigid. 不必明确指定类名使得使用非常简单且不那么严格。 Should this class be renamed or the function moved to another level in the inheritance hierarchy, you don't have to change the QE_CONST_DPTR statement. 如果要重命名此类或将函数移动到继承层次结构中的另一个级别,则不必更改QE_CONST_DPTR语句。

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

would become: 会成为:

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

One purpose of PIMPL is to decouple interface from private implementation. PIMPL的一个目的是将接口与私有实现分离。 Examples like impl->ui->component->doStuff(); 例如impl->ui->component->doStuff(); are a sign that there is a problem with the scope of the interface. 表示接口范围存在问题。 IMHO you should normally not see more than one deep calls. 恕我直言,你通常不应该看到多个深呼叫。

Ie

  • impl->doStuff(); OK
  • impl->ui->doStuff(); Hmmm, better avoid that. 嗯,更好的避免。
  • impl->ui->component->... Uh oh, things go wrong here. impl->ui->component->...哦,这里出了问题。 Caller needs to know far too much details of the implementation. 调用者需要了解实现的太多细节。 That's not the purpose of PIMPL! 这不是PIMPL的目的!

You may want to read https://herbsutter.com/gotw/_100/ , especially the section What parts of the class should go into the impl object? 您可能需要阅读https://herbsutter.com/gotw/_100/ ,特别是该类的哪些部分应该进入impl对象?

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM