How to use the Qt's PIMPL idiom?

2019-01-01 05:22发布

问题:

PIMPL stands for Pointer to IMPLementation. The implementation stands for \"implementation detail\": something that the users of the class need not to be concerned with.

Qt\'s own class implementations cleanly separate out the interfaces from the implementations through the use of the PIMPL idiom. Yet, the mechanisms provided by Qt are undocumented. How to use them?

I\'d like this to be the canonical question about \"how do I PIMPL\" in Qt. The answers are to be motivated by a simple coordinate entry dialog interface shown below.

The motivation for the use of PIMPL becomes apparent when we have anything with a semi-complex implementation. Further motivation is given in this question. Even a fairly simple class has to pull in a lot of other headers in its interface.

\"dialog

The PIMPL-based interface is fairly clean and readable.

// CoordinateDialog.h
#include <QDialog>
#include <QVector3D>

class CoordinateDialogPrivate;
class CoordinateDialog : public QDialog
{
  Q_OBJECT
  Q_DECLARE_PRIVATE(CoordinateDialog)
#if QT_VERSION <= QT_VERSION_CHECK(5,0,0)
  Q_PRIVATE_SLOT(d_func(), void onAccepted())
#endif
  QScopedPointer<CoordinateDialogPrivate> const d_ptr;
public:
  CoordinateDialog(QWidget * parent = 0, Qt::WindowFlags flags = 0);
  ~CoordinateDialog();
  QVector3D coordinates() const;
  Q_SIGNAL void acceptedCoordinates(const QVector3D &);
};
Q_DECLARE_METATYPE(QVector3D)

A Qt 5, C++11 based interface doesn\'t need the Q_PRIVATE_SLOT line.

Compare that to a non-PIMPL interface that tucks implementation details into the private section of the interface. Note how much other code has to be included.

// CoordinateDialog.h
#include <QDialog>
#include <QVector3D>
#include <QFormLayout>
#include <QDoubleSpinBox>
#include <QDialogButtonBox>

class CoordinateDialog : public QDialog
{
  QFormLayout m_layout;
  QDoubleSpinBox m_x, m_y, m_z;
  QVector3D m_coordinates;
  QDialogButtonBox m_buttons;
  Q_SLOT void onAccepted();
public:
  CoordinateDialog(QWidget * parent = 0, Qt::WindowFlags flags = 0);
  QVector3D coordinates() const;
  Q_SIGNAL void acceptedCoordinates(const QVector3D &);
};
Q_DECLARE_METATYPE(QVector3D)

Those two interfaces are exactly equivalent as far as their public interface is concerned. They have the same signals, slots and public methods.

回答1:

Introduction

The PIMPL is a private class that contains all of the implementation-specific data of the parent class. Qt provides a PIMPL framework and a set of conventions that need to be followed when using that framework. Qt\'s PIMPLs can be used in all classes, even those not derived from QObject.

The PIMPL needs to be allocated on the heap. In idiomatic C++, we must not manage such storage manually, but use a smart pointer. Either QScopedPointer or std::unique_ptr work for this purpose. Thus, a minimal pimpl-based interface, not derived from QObject, might look like:

// Foo.h
#include <QScopedPointer>
class FooPrivate; ///< The PIMPL class for Foo
class Foo {
  QScopedPointer<FooPrivate> const d_ptr;
public:
  Foo();
  ~Foo();
};

The destructor\'s declaration is necessary, since the scoped pointer\'s destructor needs to destruct an instance of the PIMPL. The destructor must be generated in the implementation file, where the FooPrivate class lives:

// Foo.cpp
class FooPrivate { };
Foo::Foo() : d_ptr(new FooPrivate) {}
Foo::~Foo() {}

See also:

  • A deeper exposition of the idiom.
  • Gotchas and pitfalls of PIMPL.

The Interface

We\'ll now explain the PIMPL-based CoordinateDialog interface in the question.

Qt provides several macros and implementation helpers that reduce the drudgery of PIMPLs. The implementation expects us to follow these rules:

  • The PIMPL for a class Foo is named FooPrivate.
  • The PIMPL is forward-declared along the declaration of the Foo class in the interface (header) file.

The Q_DECLARE_PRIVATE Macro

The Q_DECLARE_PRIVATE macro must be put in the private section of the class\'s declaration. It takes the interface class\'s name as a parameter. It declares two inline implementations of the d_func() helper method. That method returns the PIMPL pointer with proper constness. When used in const methods, it returns a pointer to a const PIMPL. In non-const methods, it returns a pointer to a non-const PIMPL. It also provides a pimpl of correct type in derived classes. It follows that all access to the pimpl from within the implementation is to be done using d_func() and **not through d_ptr. Usually we\'d use the Q_D macro, described in the Implementation section below.

The macro comes in two flavors:

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

In our case, Q_DECLARE_PRIAVATE(CoordinateDialog) is equivalent to Q_DECLARE_PRIVATE_D(d_ptr, CoordinateDialog).

The Q_PRIVATE_SLOT Macro

This macro is only needed for Qt 4 compatibility, or when targeting non-C++11 compilers. For Qt 5, C++11 code, it is unnecessary, as we can connect functors to signals and there\'s no need for explicit private slots.

We sometimes need for a QObject to have private slots for internal use. Such slots would pollute the interface\'s private section. Since the information about slots is only relevant to the moc code generator, we can, instead, use the Q_PRIVATE_SLOT macro to tell moc that a given slot is to be invoked through the d_func() pointer, instead of through this.

The syntax expected by moc in the Q_PRIVATE_SLOT is:

Q_PRIVATE_SLOT(instance_pointer, method signature)

In our case:

Q_PRIVATE_SLOT(d_func(), void onAccepted())

This effectively declares an onAccepted slot on the CoordinateDialog class. The moc generates the following code to invoke the slot:

d_func()->onAccepted()

The macro itself has an empty expansion - it only provides information to moc.

Our interface class is thus expanded as follows:

class CoordinateDialog : public QDialog
{
  Q_OBJECT /* We don\'t expand it here as it\'s 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:
  [...]
};

When using this macro, you must include the moc-generated code in a place where the private class is fully defined. In our case, this means that the CoordinateDialog.cpp file should end with:

#include \"moc_CoordinateDialog.cpp\"

Gotchas

  • All of the Q_ macros that are to be used in a class declaration already include a semicolon. No explicit semicolons are needed after Q_:

    // correct                       // verbose, has double semicolons
    class Foo : public QObject {     class Foo : public QObject {
      Q_OBJECT                         Q_OBJECT;
      Q_DECLARE_PRIVATE(...)           Q_DECLARE_PRIVATE(...);
      ...                              ...
    };                               };
    
  • The PIMPL must not be a private class within Foo itself:

    // correct                  // wrong
    class FooPrivate;           class Foo {
    class Foo {                   class FooPrivate;
      ...                         ...
    };                          };
    
  • The first section after the opening brace in a class declaration is private by default. Thus the following are equivalent:

    // less wordy, preferred    // verbose
    class Foo {                 class Foo {              
      int privateMember;        private:
                                  int privateMember;
    };                          };
    
  • The Q_DECLARE_PRIVATE expects the interface class\'s name, not the PIMPL\'s name:

    // correct                  // wrong
    class Foo {                 class Foo {
      Q_DECLARE_PRIVATE(Foo)      Q_DECLARE_PRIVATE(FooPrivate)
      ...                         ...
    };                          };
    
  • The PIMPL pointer should be const for non-copyable/non-assignable classes such as QObject. It can be non-const when implementing copyable classes.

  • Since the PIMPL is an internal implementation detail, its size is not available at the site where the interface is used. The temptation to use placement new and the Fast Pimpl idiom should be resisted as it provides no benefits for anything but a class that doesn\'t allocate memory at all.

The Implementation

The PIMPL has to be defined in the implementation file. If it is large, it can also be defined in a private header, customarily named foo_p.h for a class whose interface is in foo.h.

The PIMPL, at a minimum, is merely a carrier of the main class\'s data. It only needs a constructor and no other methods. In our case, it also needs to store the pointer to the main class, as we\'ll want to emit a signal from the main class. Thus:

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

The PIMPL is not copyable. Since we use non-copyable members, any attempt to copy or assign to the PIMPL would be caught by the compiler. Generally, it\'s best to explicitly disable the copy functionality by using Q_DISABLE_COPY.

The Q_DECLARE_PUBLIC macro works similarly to Q_DECLARE_PRIVATE. It is described later in this section.

We pass the pointer to the dialog into the constructor, allowing us to initialize the layout on the dialog. We also connect the QDialog\'s accepted signal to the internal onAccepted slot.

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
}

The onAccepted() PIMPL method needs to be exposed as a slot in Qt 4/non-C++11 projects. For Qt 5 and C++11, this is no longer necessary.

Upon the acceptance of the dialog, we capture the coordinates and emit the acceptedCoordinates signal. That\'s why we need the public pointer:

void CoordinateDialogPrivate::onAccepted() {
  Q_Q(CoordinateDialog);
  coordinates.setX(x.value());
  coordinates.setY(y.value());
  coordinates.setZ(z.value());
  emit q->acceptedCoordinates(coordinates);
}

The Q_Q macro declares a local CoordinateDialog * const q variable. It is described later in this section.

The public part of the implementation constructs the PIMPL and exposes its properties:

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() {}

The Q_D macro declares a local CoordinateDialogPrivate * const d variable. It is described below.

The Q_D Macro

To access the PIMPL in an interface method, we can use the Q_D macro, passing it the name of the interface class.

void Class::foo() /* non-const */ {
  Q_D(Class);    /* needs a semicolon! */
  // expands to
  ClassPrivate * const d = d_func();
  ...

To access the PIMPL in a const interface method, we need to prepend the class name with the const keyword:

void Class::bar() const {
  Q_D(const Class);
  // expands to
  const ClassPrivate * const d = d_func();
  ...

The Q_Q Macro

To access the interface instance from a non-const PIMPL method, we can use the Q_Q macro, passing it the name of the interface class.

void ClassPrivate::foo() /* non-const*/ {
  Q_Q(Class);   /* needs a semicolon! */
  // expands to
  Class * const q = q_func();
  ...

To access the interface instance in a const PIMPL method, we prepend the class name with the const keyword, just as we did for the Q_D macro:

void ClassPrivate::foo() const {
  Q_Q(const Class);   /* needs a semicolon! */
  // expands to
  const Class * const q = q_func();
  ...

The Q_DECLARE_PUBLIC Macro

This macro is optional and is used to allow access to the interface from the PIMPL. It is typically used if the PIMPL\'s methods need to manipulate the interface\'s base class, or emit its signals. The equivalent Q_DECLARE_PRIVATE macro was used to allow access to the PIMPL from the interface.

The macro takes the interface class\'s name as a parameter. It declares two inline implementations of the q_func() helper method. That method returns the interface pointer with proper constness. When used in const methods, it returns a pointer to a const interface. In non-const methods, it returns a pointer to a non-const interface. It also provides the interface of correct type in derived classes. It follows that all access to the interface from within the PIMPL is to be done using q_func() and **not through q_ptr. Usually we\'d use the Q_Q macro, described above.

The macro expects the pointer to the interface to be named q_ptr. There is no two-argument variant of this macro that would allow to choose a different name for the interface pointer (as was the case for Q_DECLARE_PRIVATE).

The macro expands as follows:

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

The Q_DISABLE_COPY Macro

This macro deletes the copy constructor and the assignment operator. It must appear in the private section of the PIMPL.

Common Gotchas

  • The interface header for a given class must be the first header to be included in the implementation file. This forces the header to be self-contained and not dependent on declarations that happen to be included in the implementation. If it isn\'t so, the implementation will fail to compile, allowing you to fix the interface to make it self-sufficient.

    // 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.
    
  • The Q_DISABLE_COPY macro must appear in the private section of the PIMPL

    // correct                   // wrong
    // Foo.cpp                   // Foo.cpp
    
    class FooPrivate {           class FooPrivate {
      Q_DISABLE_COPY(FooPrivate) public:
      ...                          Q_DISABLE_COPY(FooPrivate)
    };                              ...
                                 };
    

PIMPL And Non-QObject Copyable Classes

The PIMPL idiom allows one to implement copyable, copy- and move- constructible, assignable object. The assignment is done through the copy-and-swap idiom, preventing code duplication. The PIMPL pointer must not be const, of course.

Recall the in C++11, we need to heed the Rule of Four, and provide all of the following: the copy constructor, move constructor, assignment operator, and destructor. And the free-standing swap function to implement it all, of course†.

We\'ll illustrate this using a rather useless, but nevertheless correct example.

Interface

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

For performance, the move constructor and the assignment operator should be defined in the interface (header) file. They don\'t need to access the PIMPL directly:

Integer::Integer(Integer && other) : Integer() {
   swap(*this, other);
}

Integer & Integer::operator=(Integer other) {
   swap(*this, other);
   return *this;
}

All of those use the swap freestanding function, which we must define in the interface as well. Note that it is

void swap(Integer& first, Integer& second) /* nothrow */ {
   using std::swap;
   swap(first.d_ptr, second.d_ptr);
}

Implementation

This is rather straightforward. We don\'t need access to the interface from the PIMPL, thus Q_DECLARE_PUBLIC and q_ptr are absent.

// 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() {}

†Per this excellent answer: There are other claims that we should specialize std::swap for our type, provide an in-class swap along-side a free-function swap, etc. But this is all unnecessary: any proper use of swap will be through an unqualified call, and our function will be found through ADL. One function will do.