You may be wondering how a std::weak_ptr
could be useful. You'll probably wonder even more when you examine the std::weak_ptr
API. It looks anything but smart. std::weak_ptr
s can't be dereferenced, nor can they be tested for nullness. That's because std::weak_ptr
isn't a standalone smart pointer. It's an augmentation of std::shared_ptr
.
The relationship begins at birth. std::weak_ptr
s are typically created from std::shared_ptr
s. They point to the same place as the std::shared_ptr
s initializing them, but they don't affect the reference count of the object they point to:
auto spw = // after spw is constructed,
std::make_shared(); // the pointed-to Widget's
// ref count (RC) is 1. (See
// Item 21 for info on
// std::make_shared.)
…
std::weak_ptrwpw(spw); // wpw points to same Widget
// as spw. RC remains 1
…
spw = nullptr; // RC goes to 0, and the
// Widget is destroyed.
// wpw now dangles
std::weak_ptr
s that dangle are said to have expired . You can test for this directly,
if ( wpw.expired()) … // if wpw doesn't point
// to an object.
but often what you desire is a check to see if a std::weak_ptr
has expired and, if it hasn't (i.e., if it's not dangling), to access the object it points to. This is easier desired than done. Because std::weak_ptr
s lack dereferencing operations, there's no way to write the code. Even if there were, separating the check and the dereference would introduce a race condition: between the call to expired
and the dereferencing action, another thread might reassign or destroy the last std::shared_ptr
pointing to the object, thus causing that object to be destroyed. In that case, your dereference would yield undefined behavior.
What you need is an atomic operation that checks to see if the std::weak_ptr
has expired and, if not, gives you access to the object it points to. This is done by creating a std::shared_ptr
from the std::weak_ptr
. The operation comes in two forms, depending on what you'd like to have happen if the std::weak_ptr
has expired when you try to create a std::shared_ptr
from it. One form is std::weak_ptr::lock
, which returns a std::shared_ptr
. The std::shared_ptr
is null if the std::weak_ptr
has expired:
std::shared_ptr spw1 = wpw. lock(); // if wpw's expired,
// spw1 is null
auto spw2 = wpw. lock(); // same as above,
// but uses auto
The other form is the std::shared_ptr
constructor taking a std::weak_ptr
as an argument. In this case, if the std::weak_ptr
has expired, an exception is thrown:
std::shared_ptrspw3(wpw); // if wpw's expired,
// throw std::bad_weak_ptr
But you're probably still wondering about how std::weak_ptr
s can be useful. Consider a factory function that produces smart pointers to read-only objects based on a unique ID. In accord with Item 18's advice regarding factory function return types, it returns a std::unique_ptr
:
std::unique_ptr loadWidget(WidgetID id);
If loadWidget
is an expensive call (e.g., because it performs file or database I/O) and it's common for IDs to be used repeatedly, a reasonable optimization would be to write a function that does what loadWidget
does, but also caches its results. Clogging the cache with every Widget
that has ever been requested can lead to performance problems of its own, however, so another reasonable optimization would be to destroy cached Widget
s when they're no longer in use.
For this caching factory function, a std::unique_ptr
return type is not a good fit. Callers should certainly receive smart pointers to cached objects, and callers should certainly determine the lifetime of those objects, but the cache needs a pointer to the objects, too. The cache's pointers need to be able to detect when they dangle, because when factory clients are finished using an object returned by the factory, that object will be destroyed, and the corresponding cache entry will dangle. The cached pointers should therefore be std::weak_ptr
s — pointers that can detect when they dangle. That means that the factory's return type should be a std::shared_ptr
, because s td::weak_ptr
s can detect when they dangle only when an object's lifetime is managed by std::shared_ptr
s.
Here's a quick-and-dirty implementation of a caching version of loadWidget
:
std::shared_ptr fastLoadWidget(WidgetID id) {
static std::unordered_map
std::weak_ptr> cache;
auto objPtr = cache[id].lock(); // objPtr is std::shared_ptr
// to cached object (or null
// if object's not in cache)
if (!objPtr) { // if not in cache,
objPtr = loadWidget(id); // load it
cache[id] = objPtr; // cache it
}
return objPtr;
}
This implementation employs one of C++11's hash table containers ( std::unordered_map
), though it doesn't show the WidgetID
hashing and equality-comparison functions that would also have to be present.
The implementation of fastLoadWidget
ignores the fact that the cache may accumulate expired std::weak_ptr
s corresponding to Widget
s that are no longer in use (and have therefore been destroyed). The implementation can be refined, but rather than spend time on an issue that lends no additional insight into std::weak_ptr
s, let's consider a second use case: the Observer design pattern. The primary components of this pattern are subjects (objects whose state may change) and observers (objects to be notified when state changes occur). In most implementations, each subject contains a data member holding pointers to its observers. That makes it easy for subjects to issue state change notifications. Subjects have no interest in controlling the lifetime of their observers (i.e., when they're destroyed), but they have a great interest in making sure that if an observer gets destroyed, subjects don't try to subsequently access it. A reasonable design is for each subject to hold a container of std::weak_ptr
s to its observers, thus making it possible for the subject to determine whether a pointer dangles before using it.
As a final example of std::weak_ptr
's utility, consider a data structure with objects A
, B
, and C
in it, where A
and C
share ownership of B
and therefore hold std::shared_ptr
s to it:
Suppose it'd be useful to also have a pointer from B
back to A
. What kind of pointer should this be?
There are three choices:
Читать дальше