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 Widgetdestructor is no longer present. That's because we have no code to put into it. std::unique_ptrautomatically 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 sizeofor deleteto 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_ptrs is alarming, because (1) std::unique_ptris advertised as supporting incomplete types, and (2) the Pimpl Idiom is one of std::unique_ptrs 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 wis 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. pImplis a std::unique_ptr, i.e., a std::unique_ptrusing 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_assertto 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_assertthat fails, and that's usually what leads to the error message. This message is associated with the point where wis destroyed, because Widget's destructor, like all compiler-generated special member functions, is implicitly inline. The message itself often refers to the line where wis 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_ptris generated, Widget::Implis a complete type. The type becomes complete when its definition has been seen, and Widget::Implis 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_ptrdata member) only inside widget.cppafter Widget::Implhas 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.cppafter Widget::Implhas 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 Widgetprevents 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 pImplbefore reassigning it, but in the Widgetheader file, pImplpoints to an incomplete type. The situation is different for the move constructor. The problem there is that compilers typically generate code to destroy pImplin the event that an exception arises inside the move constructor, and destroying pImplrequires that Implbe 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
Читать дальше