为什么不能从unique_ptr构造weak_ptr?

84
如果我理解正确,weak_ptr不会增加所管理对象的引用计数,因此它不表示所有权。它只是让您访问由其他人管理的对象,该对象的生存期由其他人控制。
因此,我不明白为什么可以从shared_ptr构造weak_ptr,但不能从unique_ptr构造。
有人能简要解释一下吗?

13
您可能对《提案:世界上最愚蠢的智能指针》感兴趣。链接为 http://open-std.org/jtc1/sc22/wg21/docs/papers/2014/n3840.pdf。 - Chris Drew
1
这也涉及到潜在的性能影响(请参见下面我的答案)。 - The Paramagnetic Croissant
1
@ChrisDrew 嗯...我理解的对吗,observer_ptr 没有...做任何事情?它基本上只是语法糖,为那些不喜欢非拥有原始指针的人提供便利? - The Paramagnetic Croissant
@TheParamagneticCroissant 是的! - Chris Drew
1
@ChrisDrew,嘿,我在2012年向Herb Sutter提出了同样的建议,但他并没有太在意(实际上我写了两次第一次在这里然后在这里)。 - Motti
你可以使用noshared_ptr/noweak_ptr代替unique_ptr。https://github.com/xhawk18/noshared_ptr - xhawk18
10个回答

39

如果您仔细想想,weak_ptr必须引用除对象本身以外的某些内容。这是因为当没有强指针指向该对象时,对象可能会停止存在,而weak_ptr仍然必须引用一些包含信息的东西,表明对象不再存在。

对于shared_ptr而言,这个东西就是包含引用计数的内容。但是对于unique_ptr而言,没有引用计数,因此没有包含引用计数的东西,因此当对象消失时也就没有其他东西可以继续存在。所以weak_ptr就没有要引用的内容。

使用这样的weak_ptr也没有合理的方法。为了使用它,您必须有某种方式来保证在您使用它的同时对象不会被销毁。对于shared_ptr很容易做到这一点 - 这就是shared_ptr的工作方式。但是如何使用unique_ptr来实现这一点呢?您显然不能拥有两个,其他某个东西必须已经拥有对象,否则由于您的指针是弱的,对象将被销毁。


2
而C++试图避免不必要的开销,将不会为unique_ptrs负担动态分配控制块的负担,该控制块可以保存公共引用计数(与shared_ptrs相比)。 - Peter - Reinstate Monica

35

std::weak_ptr必须通过lock()转换为std::shared_ptr才能使用。如果标准允许您建议的做法,那么这意味着您需要将std::weak_ptr转换为独占指针以便使用它,违反了唯一性(或重新发明了std::shared_ptr)。

为了说明问题,请看这两个代码片段:

std::shared_ptr<int> shared = std::make_shared<int>(10);
std::weak_ptr<int> weak(shared);

{
*(weak.lock()) = 20; //OK, the temporary shared_ptr will be destroyed but the pointee-integer still has shared  to keep it alive
}

现在根据您的建议:

std::unique_ptr<int> unique = std::make_unique<int>(10);
std::weak_ptr<int> weak(unique);

{
*(weak.lock()) = 20; //not OK. the temporary unique_ptr will be destroyed but unique still points at it! 
}

话虽如此,你可能会建议只有一个unique_ptr,并且您仍然可以引用weak_ptr(而不创建另一个unique_ptr),那么就没有问题了。但这时候,unique_ptr和只有一个引用的shared_ptr之间有什么区别呢?又或者说,通过使用get获得的常规unique_ptr和C指针之间有什么区别呢?

weak_ptr并不适用于“一般的非拥有资源”,它有着非常具体的作用——主要目标是防止shared_ptr循环指向导致内存泄漏。其他任何事情都需要使用普通的unique_ptrshared_ptr来完成。


17
一个 `shared_ptr` 基本上由两部分组成:
1. 指向的对象 2. 引用计数对象
一旦引用计数降至零,对象(#1)将被删除。
`weak_ptr` 需要能够知道对象(#1)是否仍然存在。为了做到这一点,它必须能够看到引用计数对象(#2),如果它不是零,它可以为对象创建一个 `shared_ptr`(通过增加引用计数)。如果计数为零,则它将返回一个空的 `shared_ptr`。
考虑一个问题,当可以删除引用计数对象(#2)?我们必须等到没有shared_ptrweak_ptr对象引用它。为此,引用计数对象保持两个引用计数,一个是强引用,一个是弱引用。只有当这两个计数都为零时,引用计数对象才会被删除。这意味着在所有弱引用消失之后,部分内存才能被释放(这意味着使用make_shared存在隐藏的缺点)。 简而言之:weak_ptr依赖于shared_ptr中的弱引用计数,没有shared_ptr就不能有weak_ptr

3
实际上,weak_ptr 是指向引用计数对象的 shared_ptr - David Schwartz

10

从概念上讲,没有什么阻止一个实现只提供访问权限的 weak_ptr 和一个控制生命周期的 unique_ptr。然而,存在以下问题:

  • unique_ptr 一开始就不使用引用计数。添加管理弱引用的管理结构是可能的,但需要额外的动态分配。由于 unique_ptr 应该避免任何指针原始指针之上的运行时开销,因此这种开销是不可接受的。
  • 为了使用 weak_ptr 引用的对象,您需要从中提取一个“真实”的引用,首先要验证指针是否已过期,然后给您这个真实的引用(在这种情况下是一个shared_ptr)。这意味着您突然有了第二个引用指向一个应该是唯一拥有的对象,这是错误的原因。这不能通过返回一个混合半强指针来修复,因为它只会暂时延迟指针所指向的对象的可能销毁,因为您同样可以存储该指针,从而破坏 unique_ptr 的想法。

只是好奇,你在这里尝试解决什么问题,使用一个 weak_ptr


什么都没有,这只是理论上的。 - notadam
unique_ptr有开销,但只是很小的开销。Bjarne Stroustrup曾经这样说过,他支持更安全、稍微不那么高效的代码,而不是“太聪明”的代码。他非常博学,擅长引导语言朝着保持相关性的方向发展。最初的方向是追求完全的速度和零负担抽象化。如今,由于需要编写大多数有价值的东西的低级代码越来越多,而且付费程序员的成本比购买更多硬件还要高,因此开发时间少出错更重要,即使效率较低也是如此。 - user904963
你有这方面的引用吗?顺便说一下:在核心语言中投入了相当多的工作,以保证在编译时正确性,从而避免运行时开销。移动构造函数被用于曾经在C98中使用指针的地方。此外,考虑一下Rust,它将这一点提升到了另一个层次。 - Ulrich Eckhardt
@UlrichEckhardt 这里 有一个很棒的演讲,讨论了相当长时间的 unique_ptr。对于初学者来说,大多数人会写出简单的代码,但与原始指针相比,它们是巨大的。经过几次聪明地使用 noexcept 和移动语义,代码仍然不够高效。Stroustrup 知道他在谈论什么。我无法从我听他谈论 C++ 的几个小时中找到确切的分钟数。这是最近几年在 CppCon 上的一次演讲。编译器将消除所有开销,这是一个不错的猜测,但并不是真的。 - user904963

8
看起来大家都在讨论std::weak_ptr,但没有人谈论弱指针概念,而我相信这正是作者所询问的内容。
我认为没有人提到为什么标准库没有为unique_ptr提供weak_ptr。 弱指针概念并不排斥使用unique_ptr。 弱指针只是一个信息,用于判断对象是否已被删除,因此它并不是魔法,而是一个非常简单的一般化观察者模式。
这是因为线程安全和与shared_ptr的一致性。
你无法保证你的weak_ptr(从其他线程上存在的unique_ptr创建)在调用指向对象的方法时不会被销毁。 这是因为weak_ptr需要与std::shared_ptr保持一致,后者保证线程安全。 你可以实现一个能够正确处理unique_ptr的weak_ptr,但仅限于同一线程 - 在这种情况下lock方法将是不必要的。 你可以查看基于chromium的base :: WeakPtr和base :: WeakPtrFactory的源代码 - 你可以自由地使用它们与unique_ptr。
Chromium弱指针代码最可能是基于最后一个成员销毁 - 你需要添加工厂作为最后一个成员,之后我相信WeakPtr会被告知对象删除(我不确定100%)- 所以实现起来并不难。
总体而言,在IMHO中,使用unique_ptr与弱指针概念是可以的。

4

到现在为止,还没有人提到问题的性能方面,所以让我来谈一下我的看法。

weak_ptr 必须要知道对应的 shared_ptr 什么时候都超出了作用域并且指向的对象已被释放和销毁。这意味着,shared_ptr 需要以某种方式向每个 weak_ptr 发送有关相同对象的销毁通知。这会产生一定的成本 - 例如,需要更新全局哈希表,其中 weak_ptr 获取地址(如果对象被销毁,则获取 nullptr)。

这也涉及到多线程环境中的锁定,因此对于某些任务而言可能过于缓慢。

然而,unique_ptr 的目标是提供一个零代价的 RAII 风格的抽象类。因此,它不应该产生除删除动态分配对象(使用 deletedelete[])之外的任何其他成本。例如执行锁定或受保护的哈希表访问所造成的延迟可能与回收成本相当,但这对于 unique_ptr 不是理想的情况。


0
经过多年的C++编程工作,我终于意识到,在C++世界中正确的方法不是永远使用shared_ptr/weak_ptr。我们花费了很多时间来修复由share_ptr所有权不清晰引起的错误。
解决方案是使用unique_ptr和一些弱指针来代替,正如这个主题所期望的那样。
C++标准库没有unique_ptr的弱指针,因此我在这里创建了一个非常简单但有用的库——

https://github.com/xhawk18/noshared_ptr

它有两个新的智能指针,

noshared_ptr<T>, a new kind of unique_ptr
noweak_ptr<T>, the weak pointer for noshare_ptr<T>

问题是不明确的所有权,而不是shared_ptr本身。至少可以使用typedef来解决这个问题。Stroustrup在这里讨论了关于observer_ptr重新发明轮子的类似问题:https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p1408r0.pdf。 - c z

0

我用一个实现单个对象上的weak_ptr的MWE向自己展示了问题。(我在这里将其实现在X上,但X可以是任何能告诉我们何时死亡的东西,例如具有自定义删除器的unique_ptr)。

然而,最终的问题是,在某些时候,我们需要对弱指针本身进行引用计数,因为虽然X没有共享,但弱指针是共享的。这使我们完全回到了再次使用shared_ptr

也许唯一的优点是,这样做的意图更清晰,不能被违反,然而,正如 Stroustrup建议并在此答案中引用的那样, 这可以通过using语句来暗示。

#include <iostream>
#include <memory>

template<typename T>
struct ControlBlock
{
    T* Target;
    explicit ControlBlock(T* target) : Target(target) {}
};

template<typename T>
struct WeakReference
{
    std::shared_ptr<ControlBlock<T>> ControlBlock;
    T* Get() { return ControlBlock ? ControlBlock->Target : nullptr; }
};

template<typename T>
struct WeakReferenceRoot
{
    WeakReference<T> _weakRef;
    WeakReferenceRoot(T* target) : _weakRef{std::make_shared<ControlBlock<T>>(target)} { }
    const WeakReference<T>& GetReference() { return _weakRef; }
    ~WeakReferenceRoot() { _weakRef.ControlBlock->Target = nullptr; }
};

struct Customer
{
    WeakReferenceRoot<Customer> Weak{this};
};

int main() {
    WeakReference<Customer> someRef;
    std::cout << "BEFORE SCOPE - WEAK REFERENCE IS " << someRef.Get() << "\n";
    {
        Customer obj{};
        someRef = obj.Weak.GetReference();
        std::cout << "IN SCOPE - WEAK REFERENCE IS " << someRef.Get() << "\n";
    }
    std::cout << "OUT OF SCOPE - WEAK REFERENCE IS " << someRef.Get() << "\n";
    return 0;
}

0

区分喜欢使用unique_ptr而不是shared_ptr的原因可能会很有用。

性能 一个明显的原因是计算时间和内存使用。目前定义的shared_ptr对象通常需要像引用计数值这样的东西,这占用了空间并且还必须得到积极维护。unique_ptr对象则没有此问题。

语义 使用unique_ptr时,作为程序员,您对指向的对象何时将被销毁有很好的了解:当unique_ptr被销毁或调用其修改方法之一时。因此,在大型或不熟悉的代码库中,使用unique_ptr静态地传递(并强制执行)有关程序运行时行为的一些信息,这可能不太明显。

上面的注释通常集中在那些基于性能的原因上,避免将weak_ptr对象与unique_ptr对象绑定在一起。但是,人们可能会想知道语义上的论据是否足以成为STL的某个未来版本支持原始问题提出的用例的足够理由。


0

不幸的是,与许多情况一样,这是因为C++委员会并不关心并驳回了这种用例。

现状: weak_ptr是以shared-ptr为基础来进行规范的,从而排除了任何试图使其成为更广泛有用的智能指针的尝试。在C++中,概念上,弱指针是一个非拥有指针,必须转换为shared_ptr才能访问底层对象。由于unique_ptr不支持任何形式的引用计数(因为根据定义它是唯一所有者),因此不允许将weak_ptr转换为具有任何形式所有权的指针。 很遗憾,现在已经太晚了,无法获得良好命名的智能指针,它们可以提供更多的通用性。

但您可以创建类似于以下内容:

为了保证安全,您需要自己创建一个删除器(unique_ptr类型中包含删除器),以及一个新的非拥有型unique_ptr_observer,用于更改删除器。创建观察器时,它会将清理处理程序注册为删除器。这样,您就可以拥有一个unique_ptr_observer,它可以检查线程安全性是否仍然存在问题,因为您需要锁定机制、创建副本进行读取,或者其他方式来防止在您正在主动查看指针时删除指针。(真是让人烦恼,删除器居然是类型的一部分......)

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