4

So I have code that uses std::weak_ptr and maintains them in an std::set, and that works just fine -- and has worked for the last 5 or 7 years. Recently I thought I'd fiddle with using them in an std::unordered_set (well, actually in an f14::F14ValueSet) and for that, I would need a hash of it. As of now, there is no std::hash<std::weak_ptr>, so what should I do instead?

The answer seems to be "just hash the control block", as implied by this question and reply: Why was std::hash not defined for std::weak_ptr in C++0x?, but how do I get access to the control block? In glibc, it's located at __weak_ptr<>::_M_refcount._M_pi-> but that's private (and implementation specific). What else can I do?

One answer is "just wait": maybe someday there will be a standard owner_hash() for std::weak_ptr, but I'd prefer something available now.

Nick is tired
  • 6,860
  • 20
  • 39
  • 51
Linas
  • 773
  • 6
  • 13
  • 1
    Does this answer your question? [How to make a c++11 std::unordered\_set of std::weak\_ptr](https://stackoverflow.com/questions/13695640/how-to-make-a-c11-stdunordered-set-of-stdweak-ptr) – jwezorek Nov 27 '21 at 02:14
  • Looks like if you don't want to wait, you need to implement your own weak pointer type that exposes the appropriate data to make a stable hash. – JohnFilleau Nov 27 '21 at 02:15
  • 1
    Um, the answer given in https://stackoverflow.com/questions/13695640/how-to-make-a-c11-stdunordered-set-of-stdweak-ptr is clearly wrong, as it conflicts with the C++ standards committee's take on the matter. It only appears to be right in that "you can't do it", but it is wrong in explaining why. That answer should be withdrawn or modified. – Linas Nov 27 '21 at 03:51

2 Answers2

2

Make your own augmented weak ptr.

It stores a hash value, and supports == based off owner_before().

You must make these from shared_ptrs, as a weak ptr with no strong references cannot be hashed to match its owner; this could create two augmented weak ptrs that compare equal but hash differently.

template<class T>
struct my_weak_ptr {
  // weak ptr API clone goes here.  lock() etc.

  // different ctor:
  my_weak_ptr(std::shared_ptr<T>const& sp){
    if(!sp) return;
    ptr=sp;
    hash = std::hash<T*>{}(sp.get());
  }
  std::size_t getHash()const{return hash;}
  friend bool operator<(my_weak_ptr const& lhs, my_weak_ptr const& rhs){
    return lhs.owner_before(rhs);
  }
  friend bool operator!=(my_weak_ptr const& lhs, my_weak_ptr const& rhs){
    return lhs<rhs || rhs<lhs;
  }
  friend bool operator==(my_weak_ptr const& lhs, my_weak_ptr const& rhs){
    return !(lhs!=rhs);
  }
private:
  std::weak_ptr<T> ptr;
  std::size_t hash=0;
};

these have stable, sensible hashes. While a recycled object pointer results in a hash collision, so long as they don't share control blocks they won't be equal.

namespace std{template<class T>struct hash<some_ns::my_weak_ptr<T>>{
  std::size_t operator()(my_weak_ptr<T> const& wp)const{return wp.getHash();}
};}

One warning: Use of aliasing constructor could result in pathological results. As equality is based on control block equality, not pointer value.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • 1
    Storing pointer value instead of hash as a member would allow implementing equality by pointer value. While this will not be consistent with usual `weak_ptr` behavior, this may be better for aliasing constructor case – Alex Guteniev Nov 27 '21 at 09:18
  • 2
    @alex comparing and hashing dangling pointers sounds like UB to me; the restrictions on what you can do with deleted pointers in C++ are rather extreme. – Yakk - Adam Nevraumont Nov 27 '21 at 13:29
  • 1
    Oh, then cast to `uintptr_t` while it is still valid. Although this may cause other (possibly only theoretic) issues. Probably you are right that should just hash immediately. – Alex Guteniev Nov 27 '21 at 13:36
  • I'd like to edit the answer to include some minor points: saying `struct hashable_weak_ptr : public std::weak_ptr` avoids having to duplicate the needed API. Also providing `template struct owner_less>` allows it to be used in `std::set`, while `template struct hash>` is needed to complete the example. I can do these edits if that's OK? – Linas Nov 27 '21 at 21:35
  • @linas public inheritance permits slicing. So, I would not. The API is not that large. Could do private inheritance and using. Also my version has `<`; no need for extra stuff for set set? – Yakk - Adam Nevraumont Nov 27 '21 at 21:41
  • Ah yes OK re slicing. Regarding `<`: my code lives in it's own namespace, whereas `operator<` is needed in the `std` namespace. – Linas Nov 27 '21 at 22:38
  • @linas no, adl works fine for `<` – Yakk - Adam Nevraumont Nov 27 '21 at 23:35
  • Perhaps a side effect of code elsewhere. My compiler complained of a missing `owner_less`. It's a 20-year-old code base that I try to scrub clean every few years, but funny stuff leaks through anyways :-/ – Linas Nov 28 '21 at 01:40
  • @linas don't use owner less? Just `map` or whatever – Yakk - Adam Nevraumont Nov 28 '21 at 02:56
0

So I tried to implement - as proposed by Yakk - Adam Nevraumont in his answer to this question - an inheriting hashable weak-pointer impelementation including the public interface. Might anyone comment in case I got something wrong?

template<class T>
struct HashableWeakPointer : protected std::weak_ptr<T>
{
public:
    // Hash class
    class Hash
    {
    public:
        size_t operator()(HashableWeakPointer const & hashableWeakPointer) const
        {
            return hashableWeakPointer.getHash();
        }
    };


    // constructor
    HashableWeakPointer(std::shared_ptr<T> const & sp)
        : std::weak_ptr<T>(sp)
        , hash(0)
    {
        if (static_cast<bool>(sp))
        {
            hash = std::hash<T*>{}(sp.get());
        }
    }


    // weak_ptr-interface
   void reset() noexcept
   {
       std::weak_ptr<T>::reset();
       hash = 0;
   }

   void swap(HashableWeakPointer & r) noexcept
   {
       std::weak_ptr<T>::swap(r);
       std::swap(hash, r.hash);
   }

   using std::weak_ptr<T>::use_count;
   using std::weak_ptr<T>::expired;
   using std::weak_ptr<T>::lock;

   template< class Y >
   bool owner_before( const HashableWeakPointer<Y>& other ) const noexcept
   {
       return std::weak_ptr<T>::owner_before(static_cast<std::weak_ptr<Y>>(other));
   }

   template< class Y >
   bool owner_before( const std::shared_ptr<Y>& other ) const noexcept
   {
       return std::weak_ptr<T>::owner_before(other);
   }


    // hash-interface
    std::size_t getHash() const noexcept
    {
        return hash;
    }

    // helper methods

    // https://en.cppreference.com/w/cpp/memory/shared_ptr
    // "The destructor of shared_ptr decrements the number of shared owners of the control block. If that counter
    // reaches zero, the control block calls the destructor of the managed object. The control block does not
    // deallocate itself until the std::weak_ptr counter reaches zero as well."
    // So below comparisons should stay save even if all shared_ptrs to the managed instance were destroyed.

    friend bool operator<(HashableWeakPointer const& lhs, HashableWeakPointer const& rhs)
    {
        return lhs.owner_before(rhs);
    }

    friend bool operator!=(HashableWeakPointer const& lhs, HashableWeakPointer const& rhs)
    {
        return lhs<rhs || rhs<lhs;
    }

    friend bool operator==(HashableWeakPointer const& lhs, HashableWeakPointer const& rhs)
    {
        return !(lhs!=rhs);
    }

    friend std::ostream & operator<<(std::ostream & os, const HashableWeakPointer& dt)
    {
        os << "<" << dt.lock().get() << "," << dt.hash << ">";
        return os;
    }

private:

    std::size_t hash;

};

As for usage, following is a small sample code

#include <iostream>
#include <memory>
#include <unordered_map>

typedef unsigned KeyValueType;
typedef HashableWeakPointer<KeyValueType> KeyType;
typedef unsigned ValueType;
typedef std::unordered_map<KeyType, ValueType, KeyType::Hash> MapType;


int main()
{
    std::shared_ptr<KeyValueType> sharedPointer = std::make_shared<KeyValueType>(17);
    ValueType const value = 89;

    MapType map;
    std::pair<MapType::iterator,bool> const inserted = map.insert({sharedPointer, value});
    if (not inserted.second)
    {
        std::cerr << "Element for value " << value << " already existed." << std::endl;
    }

    for (MapType::value_type const & entry : map )
    {
        std::cout << "Key:[" << entry.first << "] Value:[" << entry.second << "]" << std::endl;
    }

    return 0;
}

which outputs for me [with a 64-bit size_t]:

Key:[<0x1ea4b2817f0,2105794893808>] Value:[89]

where one can see, that the value pointer is used for the hash key [2105794893808 = 0x1ea4b2817f0].