简体   繁体   中英

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.

So: we've got quite a large project with lots of GUI and other Qt classes. 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:

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

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. The opinion states, that Qt marcos also add visual garbaging to the situation in the end. Seversl #define 's could help, but those are considered generally bad practice.

In short, based on your experience, is there a way to make pimpl usage more laconic? 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

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. The PIMPL macros have nothing to do with moc. You can use them in plain C++ code that doesn't use any Qt classes at all.

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-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. 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. 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. 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. See - Qt's PIMPL implementation is verbose because you do actually need all of it to write safe, composable, maintainable code. No way around it.

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

Inheritance, Qt way

Qt's PIMPL macros take care of implementing d() functions. Well, they implement d_func() and then you use the Q_D macro to obtain a local variable that is simply d . Rewriting the above:

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

Factories simplify 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. 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.

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 ).

#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. This means your private class MUST follow the naming pattern: InterfaceClass becomes 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:

#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.

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. Examples like 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. Caller needs to know far too much details of the implementation. That's not the purpose of 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?

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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