9

I'm using weak pointers as keys in a map. However, when I tried to compile, I got ugly messages which I interpreted to mean that I lacked a comparison operator for the std::weak_ptr, which, obviously, is required in a std::map, as it orders its elements according to key value.

Now, however, the weak_ptr class is a smart-pointer type class, and, as such, works with pointers to some managed data.

Is there a good reason why this class would NOT provide a base implementation of the operator< method ? I mean, comparing the pointer values seem pretty obvious to me, and, if it is required that it works in a different way, then one should be able to extend, or redefine, the method, to get the expected behavior instead, wouldn't it ?

Your insight would be much appreciated, here. Thanks in anticipation.

Kzwix
  • 161
  • 7
  • 2
    Given that the weak pointer's underlying object can get destroyed at any time, comparing weak pointers is completely meaningless. The picosecond after `<` finishes comparing the pointers, one of the underlying object can be destroyed, so there go the results of your comparisons. This has nothing to do with pointer comparisons in generals, as the answers claim. `std::less` enforces mandatory weak ordering on pointers. Look it up. There's nothing wrong with comparing ordinary pointers. But weak pointer comparison, by their nature, is completely meaningless. – Sam Varshavchik Apr 09 '18 at 12:26
  • Ok, thanks. However, from what I understand, a weak pointer has a pointer to the "holder structure" (the thing which counts the number of smart pointers using it, and which holds the "real" pointer) of the matching shared_ptr instance(s family), right ? So, it shouldn't be that hard to use the original adress for comparison, even if the instance count is down to 0 - unless the implementation deletes this very internal structure when the pointer is "expired" (I dunno, I didn't check the internal code for the STL. Maybe I should) – Kzwix Apr 09 '18 at 14:44

3 Answers3

12

std::owner_less is the correct way to order smart pointers as keys in maps.

#include <map>
#include <memory>
#include <string>

struct Foo
{
};

using my_key = std::weak_ptr<Foo>;
using my_comp = std::owner_less<my_key>;

int main()
{
    auto m = std::map<my_key, std::string, my_comp>();

    auto p = std::make_shared<Foo>();
    auto p1 = std::make_shared<Foo>();

    m.emplace(p, "foo");
    m.emplace(p1, "bar");

    p.reset();
    p1.reset();
}
Richard Hodges
  • 68,278
  • 7
  • 90
  • 142
  • I did not know about this one. Nice find :) – rubenvb Apr 09 '18 at 13:34
  • I didn't know about it either. However, I have trouble understanding in which cases the pointers inside could be "different". The documentation I saw about std::owner_less (or about std::weak_ptr.owner_before) says that it compares the owned pointer, regardless of the result of get(), and states that those could differ "(e.g. because they point at different subobjects within the same object)" How would that work ? I mean, if I have smart pointers to class Foo, how would one of them point to a "different subobject" ? – Kzwix Apr 09 '18 at 15:07
  • @Kzwix there is a constructor of shared_ptr which allows you to share the lifetime of the controlled object, but point to a subobject of it. See note 8 in this page: http://en.cppreference.com/w/cpp/memory/shared_ptr/shared_ptr – Richard Hodges Apr 09 '18 at 21:53
  • Thanks, Richard, you made me discover another thing. However, the documentation says that it expects a pointer to an "element_type". And "element_type" is defined, in the reference, to be the type T itself (as we're not in C++17 yet). However, the constructor description explicitly states that this pointer is intended to be whatever we want, as long as it is guaranteed to be "alive" as long as the shared pointer exists. Hence, a pointer to a field, or to a downcasted version, whatever. But, I wonder, if my class Foo has a field of type Bar, how can I provide a Bar* if it expects Foo* ? – Kzwix Apr 10 '18 at 09:53
11

UPDATE: There is a well-defined way to compare std::weak_ptr, see @Richard's answer. I will leave my answer for historical archival purposes.


Implementing a good operator< would require creating a shared_ptr from the weak_ptr and calling its operator<. This is

  1. a "costly" operation
  2. undefined when the underlying shared_ptr does not exist anymore.

It would be difficult to get a well-defined and performant ordering of weak_ptrs in general.

Note that you can always pass a custom comparator to your map object, which you can implement any way you want and keep local to only that object. But then it's up to you to figure out a good way to do this. Perhaps you could use the pointer to the control block to the connected shared_ptr, but that is an implementation detail you cannot access. So I really see no meaningful way of doing this.

rubenvb
  • 74,642
  • 33
  • 187
  • 332
  • It wouldn’t be UB to compare *pointer values* (rather than pointee values), which, I think is what OP wants to do. The *actual* reason why `operator<` isn’t define is because it could give wrong results, not because it’s UB. (In particular, non-existing `shared_ptr`s can easily be handled; this is what `std::owner_less` does.) – Konrad Rudolph Apr 09 '18 at 13:00
  • @KonradRudolph it might be undefined on some systems (and implementation-defined everywhere), see [DR 1438](https://wg21.link/cwg1438). – Jonathan Wakely Apr 09 '18 at 13:47
1

I think the key point is that (quoted from C++ Concurrency in Action, p. 194, emphasis mine)

Using hazard pointers like this relies on the fact that it's safe to use the value of a pointer after the object it references has been deleted. This is technically undefined behavior if you are using the default implementation of new and delete, so either you need to ensure that your implementation permits it, or you need to use a custom allocator that permits such usage.

Providing operator<() requires reading the raw pointer value, which may result in undefined behavior (i.e., when expired() returns true). There is practically no way to guarantee that the comparison is OK, unless you first lock() and check the return value is not null.

Lingxi
  • 14,579
  • 2
  • 37
  • 93
  • The author of that book reported [a defect in the relevant wording](https://wg21.link/cwg1438), and now the standard makes it clear that comparing invalid pointer values is implementation-defined, not necessarily undefined. But that would still make it a problem for `weak_ptr` to define a comparison. – Jonathan Wakely Apr 09 '18 at 13:45