class Widget { // in "widget.h"
public:
Widget();
…
private:
struct Impl;
std::unique_ptr<Impl >pImpl; // use smart pointer
}; // instead of raw pointer
and this for the implementation file:
#include "widget.h" // in "widget.cpp"
#include "gadget.h"
#include
#include
struct Widget::Impl { // as before
std::string name; std::vector data;
Gadget g1, g2, g3;
};
Widget::Widget() // per Item 21, create
: pImpl( std::make_unique<Impl >()) // std::unique_ptr
{} // via std::make_unique
You'll note that the Widget
destructor is no longer present. That's because we have no code to put into it. std::unique_ptr
automatically deletes what it points to when it (the std::unique_ptr
) is destroyed, so we need not delete anything ourselves. That's one of the attractions of smart pointers: they eliminate the need for us to sully our hands with manual resource release.
This code compiles, but, alas, the most trivial client use doesn't:
#include "widget.h"
Widget w; // error!
The error message you receive depends on the compiler you're using, but the text generally mentions something about applying sizeof
or delete
to an incomplete type. Those operations aren't among the things you can do with such types.
This apparent failure of the Pimpl Idiom using std::unique_ptr
s is alarming, because (1) std::unique_ptr
is advertised as supporting incomplete types, and (2) the Pimpl Idiom is one of std::unique_ptr
s most common use cases. Fortunately, getting the code to work is easy. All that's required is a basic understanding of the cause of the problem.
The issue arises due to the code that's generated when w
is destroyed (e.g., goes out of scope). At that point, its destructor is called. In the class definition using std::unique_ptr
, we didn't declare a destructor, because we didn't have any code to put into it. In accord with the usual rules for compiler-generated special member functions (see Item 17), the compiler generates a destructor for us. Within that destructor, the compiler inserts code to call the destructor for Widget
's data member pImpl
. pImpl
is a std::unique_ptr
, i.e., a std::unique_ptr
using the default deleter. The default deleter is a function that uses delete on the raw pointer inside the std::unique_ptr
. Prior to using delete
, however, implementations typically have the default deleter employ C++11's static_assert
to ensure that the raw pointer doesn't point to an incomplete type. When the compiler generates code for the destruction of the Widget w
, then, it generally encounters a static_assert
that fails, and that's usually what leads to the error message. This message is associated with the point where w
is destroyed, because Widget
's destructor, like all compiler-generated special member functions, is implicitly inline
. The message itself often refers to the line where w
is created, because it's the source code explicitly creating the object that leads to its later implicit destruction.
To fix the problem, you just need to make sure that at the point where the code to destroy the std::unique_ptr
is generated, Widget::Impl
is a complete type. The type becomes complete when its definition has been seen, and Widget::Impl
is defined inside widget.cpp
. The key to successful compilation, then, is to have the compiler see the body of Widget
's destructor (i.e., the place where the compiler will generate code to destroy the std::unique_ptr
data member) only inside widget.cpp
after Widget::Impl
has been defined.
Arranging for that is simple. Declare Widget
's destructor in widget.h
, but don't define it there:
class Widget { // as before, in "widget.h"
public:
Widget();
~Widget(); // declaration only
…
private: // as before
struct Impl;
std::unique_ptr pImpl;
};
Define it in widget.cpp
after Widget::Impl
has been defined:
#include "widget.h" // as before, in "widget.cpp"
#include "gadget.h"
#include
#include
struct Widget::Impl { // as before, definition of
std::string name; // Widget::Impl
std::vector data;
Gadget g1, g2, g3;
};
Widget::Widget() // as before
: pImpl(std::make_unique()) {}
Widget::~Widget() // ~Widget definition
{}
This works well, and it requires the least typing, but if you want to emphasize that the compiler-generated destructor would do the right thing — that the only reason you declared it was to cause its definition to be generated in Widget
's implementation file, you can define the destructor body with “ = default
”:
Widget::~Widget() = default;// same effect as above
Classes using the Pimpl Idiom are natural candidates for move support, because compiler-generated move operations do exactly what's desired: perform a move on the underlying std::unique_ptr
. As Item 17explains, the declaration of a destructor in Widget
prevents compilers from generating the move operations, so if you want move support, you must declare the functions yourself. Given that the compiler-generated versions would behave correctly, you're likely to be tempted to implement them as follows:
class Widget { // still in
public: // "widget.h"
Widget();
~Widget();
Widget(Widget&& rhs) = default; // right idea,
Widget& operator=(Widget&& rhs) = default;// wrong code!
…
private: // as before
struct Impl;
std::unique_ptr pImpl;
};
This approach leads to the same kind of problem as declaring the class without a destructor, and for the same fundamental reason. The compiler-generated move assignment operator needs to destroy the object pointed to by pImpl
before reassigning it, but in the Widget
header file, pImpl
points to an incomplete type. The situation is different for the move constructor. The problem there is that compilers typically generate code to destroy pImpl
in the event that an exception arises inside the move constructor, and destroying pImpl
requires that Impl
be complete.
Because the problem is the same as before, so is the fix — move the definition of the move operations into the implementation file:
class Widget { // still in "widget.h"
public:
Widget();
~Widget();
Widget(Widget&& rhs); // declarations
Читать дальше