Widget& operator=(Widget&& rhs);// only
…
private: // as before
struct Impl;
std::unique_ptr pImpl;
};
#include // as before,
… // in "widget.cpp"
struct Widget::Impl { … }; // as before
Widget::Widget() // as before
: pImpl(std::make_unique())
{}
Widget::~Widget() = default; // as before
Widget::Widget(Widget&& rhs) = default; // defini-
Widget& Widget::operator=(Widget&& rhs) = default;// tions
The Pimpl Idiom is a way to reduce compilation dependencies between a class's implementation and the class's clients, but, conceptually, use of the idiom doesn't change what the class represents. The original Widget
class contained std::string
, std::vector
, and Gadget
data members, and, assuming that Gadget
s, like std::string
s and std::vector
s, can be copied, it would make sense for Widget
to support the copy operations. We have to write these functions ourselves, because (1) compilers won't generate copy operations for classes with move-only types like std::unique_ptr
and (2) even if they did, the generated functions would copy only the std::unique_ptr
(i.e., perform a shallow copy ), and we want to copy what the pointer points to (i.e., perform a deep copy ).
In a ritual that is by now familiar, we declare the functions in the header file and implement them in the implementation file:
class Widget { // still in "widget.h"
public:
… // other funcs, as before
Widget(const Widget& rhs); // declarations
Widget& operator=(const Widget& rhs);// only
private: // as before
struct Impl;
std::unique_ptr pImpl;
};
#include "widget.h" // as before,
… // in "widget.cpp"
struct Widget::Impl { … }; // as before
Widget::~Widget() = default; // other funcs, as before
Widget::Widget(const Widget& rhs) // copy ctor
: pImpl(std::make_unique(*rhs.pImpl))
{}
Widget& Widget::operator=(const Widget& rhs)// copy operator=
{
*pImpl = *rhs.pImpl;
return *this;
}
Both function implementations are conventional. In each case, we simply copy the fields of the Impl
struct from the source object ( rhs
) to the destination object ( *this
). Rather than copy the fields one by one, we take advantage of the fact that compilers will create the copy operations for Impl
, and these operations will copy each field automatically. We thus implement Widget
's copy operations by calling Widget::Impl
's compiler-generated copy operations. In the copy constructor, note that we still follow the advice of Item 21to prefer use of std::make_unique
over direct use of new
.
For purposes of implementing the Pimpl Idiom, std::unique_ptr
is the smart pointer to use, because the pImpl
pointer inside an object (e.g., inside a Widget
) has exclusive ownership of the corresponding implementation object (e.g., the Widget::Impl
object). Still, it's interesting to note that if we were to use std::shared_ptr
instead of std::unique_ptr
for pImpl
, we'd find that the advice of this Item no longer applied. There'd be no need to declare a destructor in Widget
, and without a user-declared destructor, compilers would happily generate the move operations, which would do exactly what we'd want them to. That is, given this code in widget.h
,
class Widget { // in "widget.h"
public:
Widget();
… // no declarations for dtor
// or move operations
private:
struct Impl;
std::shared_ptr<Impl >pImpl; // std::shared_ptr
}; // instead of std::unique_ptr
and this client code that #includes widget.h
,
Widget w1;
auto w2(std::move(w1)); // move-construct w2
w1 = std::move(w2); // move-assign w1
everything would compile and run as we'd hope: w1
would be default constructed, its value would be moved into w2
, that value would be moved back into w1
, and then both w1
and w2
would be destroyed (thus causing the pointed-to Widget::Impl
object to be destroyed).
The difference in behavior between std::unique_ptr
and std::shared_ptr
for pImpl
pointers stems from the differing ways these smart pointers support custom deleters. For std::unique_ptr
, the type of the deleter is part of the type of the smart pointer, and this makes it possible for compilers to generate smaller runtime data structures and faster runtime code. A consequence of this greater efficiency is that pointed-to types must be complete when compiler-generated special functions (e.g., destructors or move operations) are used. For std::shared_ptr
, the type of the deleter is not part of the type of the smart pointer. This necessitates larger runtime data structures and somewhat slower code, but pointed-to types need not be complete when compiler-generated special functions are employed.
For the Pimpl Idiom, there's not really a trade-off between the characteristics of std::unique_ptr
and std::shared_ptr
, because the relationship between classes like Widget
and classes like Widget::Impl
is exclusive ownership, and that makes std::unique_ptr
the proper tool for the job. Nevertheless, it's worth knowing that in other situations — situations where shared ownership exists (and std::shared_ptr
is hence a fitting design choice), there's no need to jump through the function-definition hoops that use of std::unique_ptr
entails.
Things to Remember
• The Pimpl Idiom decreases build times by reducing compilation dependencies between class clients and class implementations.
• For std::unique_ptr pImpl
pointers, declare special member functions in the class header, but implement them in the implementation file. Do this even if the default function implementations are acceptable.
• The above advice applies to std::unique_ptr
, but not to std::shared_ptr
.
Chapter 5
Rvalue References, Move Semantics, and Perfect Forwarding
When you first learn about them, move semantics and perfect forwarding seem pretty straightforward:
• Move semanticsmakes it possible for compilers to replace expensive copying operations with less expensive moves. In the same way that copy constructors and copy assignment operators give you control over what it means to copy objects, move constructors and move assignment operators offer control over the semantics of moving. Move semantics also enables the creation of move-only types, such as std::unique_ptr
, std::future
, and std::thread
.
Читать дальше