如何高效地检查两个std::weak_ptr指针是否指向同一个对象?

47
我想要比较两个std::weak_ptr或一个std::weak_ptr和一个std::shared_ptr是否相等。
我想要知道的是,这两个weak_ptr/shared_ptr所指向的对象是否相同。 比较结果应该是负的,不仅当地址不匹配时,还应该在底层对象被删除后,通过偶然的方式重新构造了相同地址的情况下也是负的。
所以基本上,我希望即使分配器保留相同的地址,这个断言也能成立:
auto s1 = std::make_shared<int>(43);
std::weak_ptr<int> w1(s1);

s1.reset();

auto s2 = std::make_shared<int>(41);
std::weak_ptr<int> w2(s2);

assert(!equals(w1,w2));

weak_ptr模板没有提供相等运算符,而我理解这是有充分理由的。
因此,一个简单的实现看起来像这样:
template <typename T, typename U>
inline bool naive_equals(const std::weak_ptr<T>& t, const std::weak_ptr<U>& u)
{
    return !t.expired() && t.lock() == u.lock();
}

template <typename T, typename U>
inline bool naive_equals(const std::weak_ptr<T>& t, const std::shared_ptr<U>& u)
{
    return !t.expired() && t.lock() == u;
}

如果第一个weak_ptr在此期间过期,它将返回0。如果没有过期,我将将weak_ptr升级为shared_ptr并比较地址。
这种方法的问题在于我必须两次锁定weak_ptr!我担心这会花费太多时间。
我想出了这个解决方案:
template <typename T, typename U>
inline bool equals(const std::weak_ptr<T>& t, const std::weak_ptr<U>& u)
{
    return !t.owner_before(u) && !u.owner_before(t);
}


template <typename T, typename U>
inline bool equals(const std::weak_ptr<T>& t, const std::shared_ptr<U>& u)
{
    return !t.owner_before(u) && !u.owner_before(t);
}

这个检查确保u的所有者块不是t的“之前”,而t不是u的“之前”,所以t == u。

这个工作是否按照我的意图进行?从不同的shared_ptr创建的两个weak_ptr是否总是以这种方式比较为非相等? 或者我漏掉了什么吗?

编辑:我为什么要这样做? 我想要一个包含共享指针的容器,并且我想要分发对其中对象的引用。 我不能使用迭代器,因为它们可能无效。我可以分发(整数)ID,但这会导致唯一性问题,并且需要一个映射类型并且会复杂化搜索/插入/删除操作。 这个想法是使用std::set,并将指针本身(封装在一个包装类中)作为键,这样客户端可以使用weak_ptr来访问集合中的对象。


1
据我理解,这是有充分理由的。如果你明白这是出于好意,那么为什么还要想要这样做呢? - Nicol Bolas
我在一个并发应用程序中遇到了这个问题,好在有owner_before()方法。对于我的使用情况,比较控制块是唯一明智的答案。 - vinipsmaker
1个回答

37
完全重写这个答案,因为我完全误解了。这是一个很棘手的事情,很难做到正确!
通常与标准一致的std::weak_ptr和std::shared_ptr的实现方式是有两个堆对象:被管理的对象和一个控制块。每个指向相同对象的共享指针都包含一个指向对象和控制块的指针,每个弱指针也是如此。控制块记录共享指针的数量和弱指针的数量,并在共享指针数量达到0时释放被管理的对象;当弱指针数量也达到0时,控制块本身也会被释放。
这一复杂性在于共享指针或弱指针中的对象指针可以指向实际被管理对象的子对象,例如基类、成员或甚至由被管理对象拥有的另一个堆对象。
S0 ----------______       MO <------+
   \__             `----> BC        |
      \_ _______--------> m1        |
     ___X__               m2 --> H  |
S1 -/      \__ __----------------^  |
    \___ _____X__                   |
    ____X________\__                |
W0 /----------------`---> CB -------+  
                          s = 2 
                          w = 1 
                               

这里我们有两个共享指针,分别指向托管对象的基类和成员,还有一个指向托管对象拥有的堆对象的弱指针;控制块记录了存在两个共享指针和一个弱指针。控制块还有一个指向托管对象的指针,在托管对象过期时用于删除该对象。 owner_before / owner_less 语义是通过比较共享和弱指针的控制块地址来进行比较,这个地址保证不会改变,除非指针本身被修改;即使弱指针因为所有共享指针被销毁而过期,其控制块仍然存在,直到所有弱指针也被销毁。
所以你的equals代码是绝对正确和线程安全的。
问题在于它与shared_ptr::operator==不一致,因为后者比较的是对象指针,而两个具有相同控制块的共享指针可以指向不同的对象(如上所述)。

为了与shared_ptr::operator==保持一致,写t.lock() == u是完全可以的;但请注意,如果它返回true,仍然不能确定弱指针是另一个共享指针的弱指针;它可能是一个别名指针,因此在后续代码中仍然可能过期。

然而,比较控制块的开销较小(因为不需要查看控制块),并且如果您不使用别名指针,将得到与==相同的结果。


C++26很有可能添加owner_equalowner_hash函数对象模板,允许在无序容器中使用weak_ptr作为键类型。有了owner_equal,比较弱指针的相等性实际上变得有意义,因为您可以安全地比较控制块指针然后对象指针,因为如果两个弱指针具有相同的控制块,则您知道两者都过期或都未过期。

2
顺便说一下,Microsoft的那个人通过在控制块内分配实际对象的方式,使用“我们知道你住在哪里”的优化方法省略了控制块中的指针。此外,请查看幻灯片,他的第4张幻灯片上有一个关于“通常”实现的漂亮图表。 - fat-lobyte
我完全同意关于 owner_hashowner_equals 的观点。我认为这样做可能会有所帮助:template <typename T> class comparable_weak_ptr { const T* m_original; std::weak_ptr<T> m_weak; public: comparable_weak_ptr(std::shared_ptr<T> shr) : m_original(shr.get()), m_weak(std::move(shr)) {} bool operator==(const std::shared_ptr<T>& shr) const { return (shr.get() == nullptr && m_original == nullptr) || shr.get() == m_original && shr == m_weak.lock(); } }; - Ben
如果您有一个指向子对象的weak_ptr,那会怎么样呢?它会与指向整个对象或任何其他子对象的weak_ptr(或shared_ptr)相等吗? - undefined
@ChrisDodd 是的,这样做应该可以;我相信这对于OP来说是令人满意的。 - undefined
2
@Ben @ecatmur:实际上,有一个提案(P1901R2)建议在C++26中添加owner_hashowner_equal,并已获得批准纳入标准中,详情请见:https://github.com/cplusplus/papers/issues/649。它已经集成在当前标准的草案中,详情请见:[mem.syn](https://eel.is/c++draft/memory.syn)。 - undefined
显示剩余3条评论

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