Enabling the Use of weak_ptr as Keys in Unordered
Associative Containers
Contents
Abstract
This paper proposes the addition of ownership based hashing and
equality, to enable the use of weak_ptr as keys in
unordered associative containers.
Motivation
I recently found myself wanting to store a collection of
weak_ptr objects. I neither had a separate key nor
any requirements on the ordering of the objects. It seemed that
using an unordered_set would be a good fit. It
quickly became apparent, however, that it’s simply not possible;
there is no way to generate a stable hash or compare
weak_ptr objects without support from the Standard
Library.
Also relevant is the discussion in LWG Issue 1406, which labelled this as not a defect, and recommended a paper be brought forward to propose the addition.
Proposal
owner_hash and owner_equal
The obvious, and in my opinion best, option is to follow in the
footsteps of owner_less, which can be used to allow
weak_ptr keys in ordered associative containers.
N2637 was the paper through which owner_less was
introduced into the standard.
The following methods and class templates would be added:
-
size_t shared_ptr::owner_hash() const noexcept; -
bool shared_ptr::owner_equal(…) const noexcept; -
size_t weak_ptr::owner_hash() const noexcept; -
bool weak_ptr::owner_equal(…) const noexcept; -
template <class T> struct owner_hash; -
template <class T> struct owner_equal;
shared_ptr and weak_ptr would gain two
new methods each: owner_hash and
owner_equal. owner_hash would return a
ownership based hash and owner_equal would compare
ownership.
In addition, two new class templates would be provided:
owner_hash and owner_equal.
This option has the benefit of providing all the tools necessary
for having a weak_ptr key in an unordered associative
container.
The addition of the methods and template specializations for
shared_ptr enables the ability to look up values in an
unordered associative container from either the
weak_ptr or the corresponding shared_ptr.
It also enables storing shared_ptr keys based on the
ownership equivalence relation, if such a thing is desired.
I don't believe this proposal exposes any more implementation
details than are already exposed through owner_less.
To satisfy the Cpp17Hash requirements that the probability
of a collision be low, a conforming implementation could simply
hash the address of the control block, per
hash<void *>.
Alternatives Considered
owner_ptr / owner / owner_id
This option would instead add the following methods:
-
const unspecified shared_ptr::owner_ptr() const noexcept; -
const unspecified weak_ptr::owner_ptr() const noexcept;
The unspecified return type would need to be something for
which there is, or could be, a std::hash
specialization and an equality comparator (for example, a pointer
to the control block, a guid stored in the control block, etc.).
This option would look to provide the bare minimum required,
leaving it up to the user to define owner_hash and
owner_equal in terms of this method.
shared_ptr_key / weak_ptr_key
This option would provide wrapper classes,
shared_ptr_key and weak_ptr_key, which
would have a std::hash specialization and an equality
comparator. They would presumably be friends of
shared_ptr and weak_ptr respectively.
Do Nothing
Whilst writing this paper, I discovered that it is actually
possible for end users to do this right now in terms of
owner_before, it just wouldn't be efficient.
owner_hash would be implemented to return a constant
value — completely going against the Cpp17Hash
requirements which state thatthe probability of a collision be
low.
and
owner_equal(p,q) would be implemented to return
!p.owner_before(q) && !q.owner_before(p).
Wording
Class template owner_hash [util.smartptr.ownerhash]
Add this entire section.
The class template owner_hash allows ownership-based
hashing.
namespace std {
template <class T> struct owner_hash;
template <class T> struct owner_hash<shared_ptr<T>> {
size_t operator()(const shared_ptr<T>&) const noexcept;
};
template <class T> struct owner_hash<weak_ptr<T>> {
size_t operator()(const weak_ptr<T>&) const noexcept;
};
}
operator()(x) shall return x.owner_hash().
Class template owner_equal [util.smartptr.ownerequal]
Add this entire section.
The class template owner_equal allows
ownership-based mixed equality comparisons of shared and
weak pointers.
namespace std {
template <class T = void> struct owner_equal;
template <class T> struct owner_equal<shared_ptr<T>> {
bool operator()(const shared_ptr<T>&, const shared_ptr<T>&) const noexcept;
bool operator()(const shared_ptr<T>&, const weak_ptr<T>&) const noexcept;
bool operator()(const weak_ptr<T>&, const shared_ptr<T>&) const noexcept;
};
template <class T> struct owner_equal<weak_ptr<T>> {
bool operator()(const weak_ptr<T>&, const weak_ptr<T>&) const noexcept;
bool operator()(const shared_ptr<T>&, const weak_ptr<T>&) const noexcept;
bool operator()(const weak_ptr<T>&, const shared_ptr<T>&) const noexcept;
};
template <> struct owner_equal<void> {
template <class T, class U>
bool operator()(const shared_ptr<T>&, const shared_ptr<U>&) const noexcept;
template <class T, class U>
bool operator()(const shared_ptr<T>&, const weak_ptr<U>&) const noexcept;
template <class T, class U>
bool operator()(const weak_ptr<T>&, const shared_ptr<U>&) const noexcept;
template <class T, class U>
bool operator()(const weak_ptr<T>&, const weak_ptr<U>&) const noexcept;
using is_transparent = unspecified;
};
}
operator()(x, y) shall return x.owner_equal(y).
Class template shared_ptr [util.smartptr.shared]
Add to the observers section:
size_t owner_hash() const noexcept; template<class U> bool owner_equal(const shared_ptr<U>& b) const noexcept; template<class U> bool owner_equal(const weak_ptr<U>& b) const noexcept;
Observers [util.smartptr.shared.obs]
Add at the end:
size_t owner_hash() const noexcept;
Returns: An unspecified value such that
— x.owner_hash() == y.owner_hash() is
true if
x.owner_equal() == y.owner_equal()
template<class U> bool owner_equal(const shared_ptr<U>& b) const noexcept; template<class U> bool owner_equal(const weak_ptr<U>& b) const noexcept;
Returns: true if *this and
b share ownership or are both empty.
Class template weak_ptr [util.smartptr.weak]
Add to the observers section:
size_t owner_hash() const noexcept; template<class U> bool owner_equal(const shared_ptr<U>& b) const noexcept; template<class U> bool owner_equal(const weak_ptr<U>& b) const noexcept;
Observers [util.smartptr.weak.obs]
Add at the end:
size_t owner_hash() const noexcept;
Returns: An unspecified value such that
— x.owner_hash() == y.owner_hash() is
true if
x.owner_equal() == y.owner_equal()
template<class U> bool owner_equal(const shared_ptr<U>& b) const noexcept; template<class U> bool owner_equal(const weak_ptr<U>& b) const noexcept;
Returns: true if *this and
b share ownership or are both empty.
Class template enable_shared_from_this [util.smartptr.enab]
It might make sense to change the example assert from
assert(!p.owner_before(q) && !q.owner_before(p)); // p and q share ownership
to
assert(p.owner_equal(q)); // p and q share ownership
References
- LWG Issue 1406 — http://www.open-std.org/jtc1/sc22/wg21/docs/lwg-closed.html#1406
-
N2637 Revisiting
std::shared_ptrcomparison — https://wg21.link/n2637 - Cpp17Hash requirements — [tab:cpp17.hash] in https://wg21.link/n4830
Acknowledgements
Thanks to Alexander Jones, Alisdair Meredith and Masud Rahman for
their help reviewing, suggesting alternatives, and providing
background on the history of owner_less.