std::shared_ptr spw(new Widget, cusDel);
processWidget(spw, computePriority()); // correct, but not
// optimal; see below
This works, because a std::shared_ptr
assumes ownership of the raw pointer passed to its constructor, even if that constructor yields an exception. In this example, if spw
's constructor throws an exception (e.g., due to an inability to dynamically allocate memory for a control block), it's still guaranteed that cusDel
will be invoked on the pointer resulting from “ new Widget
”.
The minor performance hitch is that in the exception-unsafe call, we're passing an rvalue to processWidget
,
processWidget(
std::shared_ptr(new Widget, cusDel), // arg is rvalue
computePriority()
);
but in the exception-safe call, we're passing an lvalue:
processWidget( spw, computePriority()); // arg is lvalue
Because processWidget
's std::shared_ptr
parameter is passed by value, construction from an rvalue entails only a move, while construction from an lvalue requires a copy. For std::shared_ptr
, the difference can be significant, because copying a std::shared_ptr
requires an atomic increment of its reference count, while moving a std::shared_ptr
requires no reference count manipulation at all. For the exception-safe code to achieve the level of performance of the exception-unsafe code, we need to apply std::move
to spw
to turn it into an rvalue (see Item 23):
processWidget( std::move(spw ), // both efficient and
computePriority()); // exception safe
That's interesting and worth knowing, but it's also typically irrelevant, because you'll rarely have a reason not to use a make
function. And unless you have a compelling reason for doing otherwise, using a make
function is what you should do.
Things to Remember
• Compared to direct use of new, make functions eliminate source code duplication, improve exception safety, and, for std::make_shared
and std::allocate_shared
, generate code that's smaller and faster.
• Situations where use of make functions is inappropriate include the need to specify custom deleters and a desire to pass braced initializers.
• For std::shared_ptr
s, additional situations where make functions may be ill-advised include (1) classes with custom memory management and (2) systems with memory concerns, very large objects, and std::weak_ptr
s that outlive the corresponding std::shared_ptr
s.
Item 22: When using the Pimpl Idiom, define special member functions in the implementation file.
If you've ever had to combat excessive build times, you're familiar with the Pimpl (“pointer to implementation”) Idiom . That's the technique whereby you replace the data members of a class with a pointer to an implementation class (or struct), put the data members that used to be in the primary class into the implementation class, and access those data members indirectly through the pointer. For example, suppose Widget
looks like this:
class Widget { // in header "widget.h"
public:
Widget();
…
private:
std::string name;
std::vector data;
Gadget g1, g2, g3; // Gadget is some user-
}; // defined type
Because Widget
's data members are of types std::string
, std::vector
, and Gadget
, headers for those types must be present for Widget
to compile, and that means that Widget
clients must #include
, , and gadget.h
. Those headers increase the compilation time for Widget
clients, plus they make those clients dependent on the contents of the headers. If a header's content changes, Widget
clients must recompile. The standard headers and don't change very often, but it could be that gadget.h
is subject to frequent revision.
Applying the Pimpl Idiom in C++98 could have Widget
replace its data members with a raw pointer to a struct that has been declared, but not defined:
class Widget { // still in header "widget.h"
public:
Widget();
~Widget(); // dtor is needed-see below
…
private:
struct Impl; // declare implementation struct
Impl *pImpl; // and pointer to it
};
Because Widget
no longer mentions the types std::string
, std::vector
, and Gadget
, Widget
clients no longer need to #include
the headers for these types. That speeds compilation, and it also means that if something in these headers changes, Widget
clients are unaffected.
A type that has been declared, but not defined, is known as an incomplete type . Widget::Impl
is such a type. There are very few things you can do with an incomplete type, but declaring a pointer to it is one of them. The Pimpl Idiom takes advantage of that.
Part 1 of the Pimpl Idiom is the declaration of a data member that's a pointer to an incomplete type. Part 2 is the dynamic allocation and deallocation of the object that holds the data members that used to be in the original class. The allocation and deallocation code goes in the implementation file, e.g., for Widget
, in widget.cpp
:
#include "widget.h" // in impl.file "widget.cpp"
#include "gadget.h"
#include
#include
struct Widget::Impl { // definition of Widget::Impl
std::string name; // with data members formerly
std::vector data; // in Widget
Gadget g1, g2, g3;
};
Widget::Widget() // allocate data members for
: pImpl( new Impl) // this Widget object
{}
Widget::~Widget() // destroy data members for
{ delete pImpl;} // this object
Here I'm showing #include
directives to make clear that the overall dependencies on the headers for std::string
, std::vector
, and Gadget
continue to exist. However, these dependencies have been moved from widget.h
(which is visible to and used by Widget
clients) to widget.cpp
(which is visible to and used only by the Widget
implementer). I've also highlighted the code that dynamically allocates and deallocates the Impl
object. The need to deallocate this object when a Widget
is destroyed is what necessitates the Widget
destructor.
But I've shown you C++98 code, and that reeks of a bygone millennium. It uses raw pointers and raw new
and raw delete
and it's all just so…raw. This chapter is built on the idea that smart pointers are preferable to raw pointers, and if what we want is to dynamically allocate a Widget::Impl
object inside the Widget
constructor and have it destroyed at the same time the Widget
is, std::unique_ptr
(see Item 18) is precisely the tool we need. Replacing the raw pImpl
pointer with a std::unique_ptr
yields this code for the header file,
Читать дальше