如何创建一个由std::weak_ptr组成的C++11 std::unordered_set?

15

我有这样一个集合:set<weak_ptr<Node>, owner_less<weak_ptr<Node> > > setName;

它可以正常工作。但是我想将其改为无序集合,但这样做会导致大约六页的错误。有什么想法吗?

在查看了所有错误消息页面后,我找到了两行可能有帮助的代码。

/usr/include/c++/4.7/bits/functional_hash.h:60:7: error: static assertion failed: std::hash is not specialized for this type
/usr/include/c++/4.7/bits/stl_function.h: In instantiation of ‘bool std::equal_to<_Tp>::operator()(const _Tp&, const _Tp&) const [with _Tp = std::weak_ptr<Node>]’:

“我有一个这样的集合:set,owner_less>> setName;” 这句话是什么意思? - Nicol Bolas
@jogojapan 谢谢,我已经尝试过了,但没有帮助。 - user1404617
@JoachimPileborg,我只问了五个问题,只有一个得到了回答,而那个回答是我自己写的。所以这就是为什么我从来没有接受过任何答案。感谢您的提醒。 - user1404617
你有八个问题,其中六个有答案。两个问题有多个答案。即使你自己写答案,你仍然可以接受它。 - Some programmer dude
4个回答

27
短暂而不幸的答案是,虽然可以安全地将shared_ptr<>用作无序集或映射中的键,但weak_ptr<>不能也不应该这样做。任何诡计都不能使它变得安全。
这是因为weak_ptr的接口没有公开访问共享控制对象,而比较使用owner_before()时,则是有序集或映射的基础。
虽然锁定指针然后哈希shared_ptr可能看起来合理,但实际上并非如此。如果最后一个shared_ptr超出范围,则哈希值将更改,这将导致下次迭代集或映射时产生未定义的行为。这通常会在代码处于客户机制造业生产环境时才被注意到,导致功能的意外和难以解释的丢失。但你的单元测试仍然会无误地通过,给你错误的想法,认为你的测试覆盖率很好,你的代码可靠,并且是用户、硬件或网络的问题。
因此,总结一下,如果你要使用weak_ptr构建你的非拥有对象缓存(对于它们来说非常优秀),则需要使用std::set<weak_ptr>,并忍受微小的性能损失(虽然实际上这将被保护集的mutex造成的性能损失所掩盖)。
如果你真的想将weak_ptr用作无序键,则必须编写自己的代码(提示:使用共享控制块的地址作为哈希函数的基础)。

我稍微思考了一下。由于weak_ptr不提供对其内部状态的任何访问,所以这有点棘手。那么一个包含weak_ptr <T>const T *的包装类怎么样?在构造时,将T *设置为weak.lock().get(),并用于散列,但相等性测试将与weak.lock()进行比较。这样,T可以被销毁而哈希值不会改变,但是相等性会改变。 - Ben
2
@Ben,这假设地址从未被用于两个不同的对象。但这并不一定正确。 - Richard Hodges
1
@Ben,这个程序注定要失败。如果你要包装weak_ptr,那么你需要为每个对象生成一个唯一的标识符。每个创建的weak_ptr都需要知道这个唯一标识符。既然如此,为什么不将unique_id作为unordered_map的键呢? - Richard Hodges
1
我看到问题了。如果标准提供了类似于 owner_before 的哈希,会怎样呢?在我的实现中,std::weak_ptr<T>::owner_before(const weak_ptr<U>& p) 只是 { return __cntrl_ < p.__cntrl_; },其中 __cntrl_ 是指向控制块的指针。因此,如果 std::weak_ptr 提供了一个 std::size_t unique_id() const 方法,将 __cntrl_ 转换为 std::size_t,我们就可以以此为关键字进行哈希。 - Ben
@Ben 是的,很遗憾那没有被提供。 - Richard Hodges
显示剩余6条评论

11

我认为建议的哈希函数不正确。如果指向该对象的所有共享指针都消失了,那么weak_ptr<X>::lock()将返回空的shared_ptr,其哈希值可能为零。因此,哈希函数可能会在不同时间返回不同的值。

我认为这里的正确解决方案是使用boost::unordered_map<X*, boost::weak_ptr<X>>。类型X*可以轻松用作哈希映射的键,而值weak_ptr<X>则可以让您查找被引用的对象是否仍然存在。

要将值存储到此哈希表中,您可以使用类似以下代码:

if (boost::shared_ptr<X> p = wp.lock()) {
    // weak_ptr is still valid
    ptrs.insert(std::make_pair(p.get(), p));
}

这使用可能已过期的对象的内存位置作为键,这意味着您需要考虑要插入的对象可能已分配在与先前对象相同的位置,其键仍然存在于映射中的情况。 - John
1
weka_ptr<...> 确保之前的对象是否仍然存在。如果它还活着,就不能有相同地址的不同对象,如果它不存在,则可以替换它。如果您没有 multimap,则 Insert 不允许两次插入具有相同键的 <K,V>,因此上述语句实际上应该检查插入是否成功,并相应地采取行动。 - faramir

1
请阅读Richard Hodges的答案,因为我的答案虽然被接受,但是是不正确的。

Since unordered_sets are hash-based you have to provide a hash function object for the std::weak_ptr data-type.

If you take a look at the unordered_set template-parameters

template<class Key,
    class Hash = std::hash<Key>,
    class Pred = std::equal_to<Key>,
    class Alloc = std::allocator<Key> >
    class unordered_set;

you'll notice that std::unordered_set provides you with a default std::hash<> template parameter. But since std::hash does only provide specializations for a specific set of data types, you might have to provide your own.

The error-message you quoted tells you, that no std::hash<> specialization for std::weak_ptr<> exists, so you have to provide your own hashing function for that:

template<typename T>
struct MyWeakPtrHash : public std::unary_function<std::weak_ptr<T>, size_t> {
   size_t operator()(const std::weak_ptr<T>& wp)
   {
      // Example hash. Beware: As zneak remarked in the comments* to this post,
      // it is very possible that this may lead to undefined behaviour
      // since the hash of a key is assumed to be constant, but will change
      // when the weak_ptr expires
      auto sp = wp.lock();
      return std::hash<decltype(sp)>()(sp);
   }
};

Edit: You also need to provide an equality function, since no std::equal_to for weak_ptr is provided. Taking a possible way to do this from "Equality-compare std::weak_ptr" on Stackoverflow:

template<typename T>
struct MyWeakPtrEqual : public std::unary_function<std::weak_ptr<T>, bool> {

   bool operator()(const std::weak_ptr<T>& left, const std::weak_ptr<T>& right)
   {
      return !left.owner_before(right) && !right.owner_before(left);
   }
};

All combined this gives us the following:

std::unordered_set<std::weak_ptr<T>,
                   MyWeakPtrHash<T>,
                   MyWeakPtrEqual<T>> wpSet;

能否让MyWeakPtrHash接受weak_ptr<Node>,然后将其转换为shared_ptr并获取其哈希值并返回。我尝试了几种不同的方法,但没有编译成功。 - user1404617
由于std::hash为std::shared_ptr提供了一个专门的实现,因此您可以利用它。我更新了示例以利用这一点。但我不知道这是否能按预期工作... :| - lx.
19
假设键的哈希值是常量,但您的函数允许对其进行修改:如果 weak_ptr 过期,wp.lock() 将返回一个空的 shared_ptr,并具有不同的哈希值。这将导致未定义的行为。 - zneak
将此与“T=void”一起使用会导致有关限定符被丢弃的错误。为什么?如何解决? - tambre

1
这里给出了一个可行的解决方案:如何计算std::weak_ptr 的哈希值?以下是稍微扩展的版本,添加了缺失的细节。与之前给出的回答不同的是,这个方法能够工作是因为哈希在 shared_ptr 计数降至零之前就已经被计算并存储了。
namespace foobar
{
// Public inheritance was used to avoid having to
// duplicate the rest of the API. Unfortunately this
// allows object slicing. So, an alternate solution is
// to use private inheritance, and `using` to provide
// the missing API.
template<class T>
struct hashable_weak_ptr : public std::weak_ptr<T>
{
   hashable_weak_ptr(std::shared_ptr<T>const& sp) :
      std::weak_ptr<T>(sp)
   {
      if (!sp) return;
      _hash = std::hash<T*>{}(sp.get());
   }

   std::size_t get_hash() const noexcept { return _hash; }

   // Define operator<() in order to construct operator==()
   // It might be more efficient to store the unhashed
   // pointer, and use that for equality compares...
   friend bool operator<(hashable_weak_ptr const& lhs,
                         hashable_weak_ptr const& rhs)
   {
      return lhs.owner_before(rhs);
   }
   friend bool operator!=(hashable_weak_ptr const& lhs,
                          hashable_weak_ptr const& rhs)
   {
      return lhs<rhs or rhs<lhs;
   }
   friend bool operator==(hashable_weak_ptr const& lhs,
                          hashable_weak_ptr const& rhs)
   {
      return not (lhs != rhs);
   }
   private:
      std::size_t _hash = 0;
};
} // namespace foobar

namespace std
{

// Specializations in std namespace needed
// for above to be usable.
template<class T>
struct owner_less<foobar::hashable_weak_ptr<T>>
{
   bool operator()(const foobar::hashable_weak_ptr<T>& lhs,
                   const foobar::hashable_weak_ptr<T>& rhs) const noexcept
   {
      return lhs.owner_before(rhs);
   }
};

template<class T>
struct hash<foobar::hashable_weak_ptr<T>>
{
   std::size_t operator()(const foobar::hashable_weak_ptr<T>& w) const noexcept
   {
      return w.get_hash();
   }
};
} // namespace std

这个问题的一个变体首先在这里被提出:为什么C++0x中没有为std::weak_ptr定义std::hash? 解决这个问题的最新标准委员会草案在这里:JTC1 WG21 P1901

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接