1. Introduction
2. Problem: Fine control over weak_ptr
ownership
3. Problem: Generic programming and weak_ptr
4. Solutions
5. One more motivating example
6. Possible objections
6a. Does weak_ptr
deserve parity with shared_ptr
?
6b. Could unlock
be confused with reset
?
6c. Could weak_ptr::unlock()
be a free function instead?
6d. Should weak_ptr
also have certain move-constructors?
7. Proposed wording
20.8.2.2 Class template shared_ptr [util.smartptr.shared]
20.8.2.2.5 shared_ptr observers [util.smartptr.shared.obs]
20.8.2.3 Class template weak_ptr [util.smartptr.weak]
20.8.2.3.1 weak_ptr constructors [util.smartptr.weak.const]
8. References
This paper identifies two minor inconveniences in the design of shared_ptr
and weak_ptr
, diagnoses them as owing to unnecessary asymmetry
between those two classes, and proposes wording to eliminate the asymmetry
(and thus the inconveniences).
weak_ptr
ownershipConsider the following code sample:
shared_ptr<int> first_elt_strong(const shared_ptr<vector<int>>& vec) { return shared_ptr<int>(vec, &vec->at(0)); }
The first_elt_strong
function returns a "strong reference" (a shared_ptr
)
to the first element of the vector passed in as *vec
. Naturally, we want the returned
object (call it r
) to share ownership with vec
, so that the controlled
vector doesn't get deallocated before the last user of r
.
Fortunately, there is a standard solution to this problem:
shared_ptr
has a two-argument constructor that allows us to set the
ownership of the constructed object separately from the stored pointer.
This functionality was added to the standard in proposal N2351 (July 2007) [N2351].
That proposal presented the following rationale:
Advanced users often require the ability to create a
shared_ptr
instancep
that shares ownership with another (master)shared_ptr q
but points to an object that is not a base of*q
.*p
may be a member or an element of*q
, for example. This section proposes an additional constructor that can be used for this purpose.
That rationale applies in full force to this proposal, if you replace "shared_ptr
"
throughout with "weak_ptr
".
weak_ptr
looks deceptively similar on
the surface:
weak_ptr<int> first_elt_weak(const shared_ptr<vector<int>>& vec) { return shared_ptr<int>(vec, &vec->at(0)); }However, the
return
statement hides a non-trivial amount of code. This code
first constructs a shared_ptr
(incrementing the strong reference count of the
controlled object), then uses that shared_ptr
to initialize a return value
of type weak_ptr
(incrementing the weak reference count), then destroys the
shared_ptr
(decrementing the strong reference count): three atomic accesses,
when just one would suffice.
weak_ptr
Given an arbitrary shared_ptr
, write code to "weaken" the object into
a weak_ptr
with the same shared ownership and stored pointer.
template<class ObjectType> void register_observers(ObjectType& obj) { auto sptr = obj.get_shared_ptr(); // for example, via shared_from_this auto wptr = weak_ptr<ObjectType>(sptr); sptr.reset(); // drop the strong reference as soon as possible register_observer_1(wptr); register_observer_2(wptr); }
Unfortunately, this code is not perfectly generic: it will fail (or at least
do the wrong thing) in the case that obj.get_shared_ptr()
returns something other than shared_ptr<ObjectType>
.
For example, it might return shared_ptr<BaseClassOfObjectType>
.
Or, in an advanced scenario, we might want to be able to "drop in" a
replacement class custom_shared_ptr
instead of the standard
shared_ptr
, in which case we would also have to replace
weak_ptr
in the above code with custom_weak_ptr
.
Notice that the only place in the code sample where an explicit
type is used, is in the line concerned with "weakening" sptr
.
"Weakening" a shared_ptr
into a weak_ptr
is not
an operation that ought to force explicit types into otherwise generic code.
We propose adding two new functionalities to the standard library.
First, weak_ptr
gains a two-argument constructor to allow
direct construction of weak_ptr
objects with a specific
shared ownership and a specific stored pointer. This constructor
is exactly symmetrical with the existing two-argument constructor
of shared_ptr
, and allows us to rewrite our first problematic
code sample (using only one atomic access) as:
weak_ptr<int> first_elt_weak(const shared_ptr<vector<int>>& vec) { return weak_ptr<int>(vec, &vec->at(0)); }
Second, for creating a weak_ptr<T>
directly from
a shared_ptr<T>
in the context of generic programming
where the type T
may not be easily accessible, shared_ptr
gains a const member function shared_ptr<T>::unlock()
.
This member function
is exactly symmetrical with the existing functionality for creating
a shared_ptr<T>
directly from a weak_ptr<T>
,
which is known as weak_ptr<T>::lock()
. It allows us to
rewrite our second problematic code sample (in generic style, naming
no types explicitly) as:
template<class ObjectType> void register_observers(ObjectType& obj) { auto sptr = obj.get_shared_ptr(); // for example, via shared_from_this auto wptr = sptr.unlock(); sptr.reset(); // drop the strong reference as soon as possible register_observer_1(wptr); register_observer_2(wptr); }
A real-world example of this pattern cropped up recently when Arthur attempted
to implement a "task-based programming" library with task cancellation, as
described in Sean Parent's "Better Code: Concurrency" [Parent].
In this library, the TaskControlBlock
is the central concept;
a Future
is simply a thin wrapper around a
std::shared_ptr<TaskControlBlock>
, and a Promise
is a thin wrapper around a std::weak_ptr<TaskControlBlock>
.
When the last Future
referring to a TaskControlBlock
is destroyed, the TaskControlBlock
itself (if the task has not
yet begun to execute) may be destroyed. The implementation of
EnqueueTask
in this system looks like this:
template<typename T> inline std::weak_ptr<T> Unlock(const std::shared_ptr<T>& sptr) { return std::weak_ptr<T>(sptr); } template<typename Func> auto EnqueueTask(Func&& func) -> Future<decltype(func())> { using T = decltype(func()); auto sptr = std::make_shared<TaskControlBlock<T>>(); auto task = [ func = std::forward<Func>(func), wptr = Unlock(sptr) ]() { Promise<T> px { wptr }; px.set_value(func()); }; GlobalTaskPool.submit(task); Future<T> fx { std::move(sptr) }; return fx; }Notice the use of the hand-coded free function
Unlock(sptr)
; that's
a workaround for the unwieldy expression std::weak_ptr<TaskControlBlock<T>>(sptr)
.
If this proposal is adopted, the expression will become sptr.unlock()
.
weak_ptr
deserve parity with shared_ptr
?
It has been suggested that the appropriate intuition for weak_ptr
is not to think of it
as a "pointer" type, but as a contract, promise, or "IOU" that may be redeemed (via lock
)
to produce a pointer when needed [Rodriguez]. There is merit in this idea.
weak_ptr
is certainly not a normal "pointer" type: it has no unary operator*
or operator->
. Therefore, is there any logic at all to creating symmetry
between shared_ptr
(a "pointer" type) and weak_ptr
(a "non-pointer" type)?
We believe that viewing weak_ptr
as an "IOU" type (rather than a "pointer" type)
doesn't affect any of the problems or solutions presented in this proposal. For example, if we
describe the purpose of lock
as "redeem an IOU to obtain a shared_ptr
,"
it seems reasonable to provide a (symmetric) member function unlock()
that, given
a real shared_ptr
, can turn it into an IOU to be redeemed later.
The arguments from efficiency (number of reference-count bumps) and "generic programmability"
(ability to express a conversion to weak_ptr
without explicit types) seem no less
applicable, whether one sees shared_ptr
and weak_ptr
on equal
footing or whether one sees weak_ptr
as an IOU to produce a
shared_ptr
.
unlock
be confused with reset
?The following code has no bugs:
void process() { auto sptr = get_shared_ptr_from_somewhere(); // let's hang onto it while doing some work do_work(); // now it's safe to drop the strong reference and possibly destroy the state sptr.reset(); // at this point the state will have been destroyed if there are no other references to it do_other_work(); }However, if a careless coder were to accidentally type
sptr.unlock()
(by analogy with mutex::unlock()
) instead of sptr.reset()
,
the code would fail to drop the strong reference and would therefore break the invariants that
might be assumed by do_other_work()
.
That is, while the non-static member functions mutex::lock()
and
mutex::unlock()
are mutators, the non-static member functions
weak_ptr::lock()
and shared_ptr::unlock()
are observers:
they do not modify *this
, but rather return brand-new objects and
leave the old *this
untouched.
If it were not too late to
rename lock
, we would suggest pairs such as
wptr.strengthed() / sptr.weakened()
or wptr.as_shared() / sptr.as_weak()
.
But, as it is too late, in our opinion the symmetry of wptr.lock()
and sptr.unlock()
is desirable enough to outweigh the advantages of
any alternative naming scheme.
This is not a new class of problem; for example, careless coders have been observed in the wild
typing vector.empty()
instead of vector.clear()
[Bug25509].
It might be wise of implementors to annotate both lock()
and unlock()
(and empty()
) with implementation-specific attributes such as
[[warn_unused_result]]
.
weak_ptr::unlock()
be a free function instead?Other commentators (before this proposal), for example [Hall] and [Bolas], have suggested code along the lines of
template<typename T> std::weak_ptr<T> to_weak(const std::shared_ptr<T>& sptr) { return std::weak_ptr<T>(sptr); }This seems like a reasonable and generic-friendly approach; in fact a free function (or somehow wrapping the function up into a traits class?) would be more friendly to generic programming, on the face of it.
The fundamental argument against a free function is symmetry. It would be
unfortunate if the opposite of wptr.lock()
were spelled
to_weak(sptr)
. We could propose to add to_strong(wptr)
at the same time... but the more symmetry we do add, the more glaringly
the few exceptions will stand out! Unfortunately, the asymmetry of lock
versus
unlock
will not go away by any route other than adding unlock
to the library.
weak_ptr
also have certain move-constructors?Between working drafts N3797 and N4296, weak_ptr
gained a move constructor
and move assignment operator, as part of the resolution of LWG issue #2315
[DR2315].
Since one of the present proposal's code samples involves creating a weak reference from a strong reference and then immediately dropping the strong reference, one might reasonably ask whether this functionality should be supported via a constructor such as
weak_ptr(shared_ptr<T>&& r) noexcept;We believe the answer is no, because users would certainly expect this constructor to be both efficient and atomic, and implementors might find it difficult to achieve either of those goals. In the most obvious implementation, the constructor would have to simultaneously (atomically) increment a "weak reference count" and decrement a "strong reference count".
Interestingly, this code currently compiles, but with "wrong" semantics:
void register_observers(std::shared_ptr<int> sptr) { std::weak_ptr<int> wptr(std::move(sptr)); // destructively convert to a weak reference? register_observer_1(wptr); register_observer_2(wptr); }The instance
sptr
does not become empty as it would if it were
"moved into" another instance of shared_ptr
. Instead, the rvalue reference
quietly degrades itself into a const lvalue reference, and sptr
keeps its
old value.
This "problem" could be "fixed" by judicious deletion of weak_ptr
and
shared_ptr
constructors and assignment operators that take rvalue references
of the other type; but that might have a significant effect on existing codebases,
and is outside the scope of the present proposal regardless.
The wording in this section is relative to WG21 draft standard N4296 [N4296].
Edit paragraph 1 as follows.
// 20.8.2.2.5, observers:
T* get() const noexcept;
T& operator*() const noexcept;
T* operator->() const noexcept;
long use_count() const noexcept;
bool unique() const noexcept;
explicit operator bool() const noexcept;
weak_ptr<T> unlock() const noexcept;
template<class U> bool owner_before(shared_ptr<U> const& b) const;
template<class U> bool owner_before(weak_ptr<U> const& b) const;
Add a new paragraph between the existing paragraphs 11 and 12.
weak_ptr<T> unlock() const noexcept;
Returns:
weak_ptr<T>(*this)
.
Edit paragraph 1 as follows.
// 20.8.2.3.1, constructors
constexpr weak_ptr() noexcept;
template<class Y> weak_ptr(shared_ptr<Y> const& r) noexcept;
template<class Y> weak_ptr(shared_ptr<Y> const& r, T* p) noexcept;
weak_ptr(weak_ptr const& r) noexcept;
template<class Y> weak_ptr(weak_ptr<Y> const& r) noexcept;
template<class Y> weak_ptr(weak_ptr<Y> const& r, T* p) noexcept;
weak_ptr(weak_ptr&& r) noexcept;
template<class Y> weak_ptr(weak_ptr<Y>&& r) noexcept;
Add a new paragraph after the existing paragraph 5.
template<class Y> weak_ptr(weak_ptr<Y> const& r, T* p) noexcept;
template<class Y> weak_ptr(shared_ptr<Y> const& r, T* p) noexcept;Effects: Constructs a
weak_ptr
instance that storesp
and shares ownership withr
.Postconditions:
use_count() == r.use_count()
.
to_weak_ptr
function that takes
a shared_ptr<T>
and returns an appropriate weak_ptr<T>
."
https://groups.google.com/a/isocpp.org/forum/#!msg/std-proposals/fBe0AySrnKY/a3Ww4SWwJDUJ
v.empty(); // v.clear() was intended!
"weak_ptr
is not a pointer but a mechanism to (possibly) obtain one. ... I don't believe there is value
of symmetry between a pointer and a non-pointer type."