【问题标题】:Why does std::unique_ptr prevent access to the object being destroyed? [duplicate]为什么 std::unique_ptr 阻止访问被破坏的对象? [复制]
【发布时间】:2021-09-04 14:48:21
【问题描述】:

据我所知,下面的代码是正确的:

#include <iostream>
#include <functional>

struct A
{
    std::function<void ()> func;
    
    int value = 5;
    
    ~A() { func(); }
};

int main()
{
    A* p_a = new A();
    p_a->func = [&p_a]() { std::cout << p_a->value; };
    delete p_a;
    return 0;
}

但下面的代码不是,会导致分段错误:

#include <memory>

int main()
{
    std::unique_ptr<A> p_a = std::make_unique<A>();
    p_a->func = [&p_a]() { std::cout << p_a->value; };
    p_a = {};
    return 0;
}

std::unique_ptr 首先清除其内部指针,然后删除对象,从而防止访问被销毁的对象。

它是干什么用的?这背后的逻辑是什么?

EDIT1:

标记的问题是重复的。如果删除器抛出异常(如果我没有误解的话),我更喜欢 std::uniqu_ptr 保留旧对象。

【问题讨论】:

  • This 似乎有些相关。
  • 另一方面,第一个示例没有产生分段错误并不意味着它是有效的。
  • 第二个例子是调用带有智能指针引用的函数。在调用析构函数时,智能指针已被重置为 nullptr。取消引用 nullptr 是未定义的行为。一种解决方法是p_a-&gt;func = [&amp;p_a]() { if (p_a) std::cout &lt;&lt; p_a-&gt;value &lt;&lt; "\n"; };
  • @super A::func 在析构函数开始执行时仍然存在。
  • @Eljay 这就是 OP 所说的。我想问题是 - 至少我是怎么理解的 - 为什么顺序不是:首先删除当前管理的对象,然后用nullptr替换它。

标签: c++


【解决方案1】:

这个实现是为了防止std::unique_ptr::reset()中的递归。

当您通过引用获取std::unique_ptr 时,您不拥有std::unique_ptr,也无法控制被引用对象的生命周期。正确的代码必须始终检查 nullptr 原始指针。

int main()
{
    std::unique_ptr<A> p_a = std::make_unique<A>();
    p_a->func = [&p_a]() { if (p_a) std::cout << p_a->value; };
    p_a = {};
    return 0;
}

【讨论】:

  • 啊,好吧,所以如果用户执行p_a-&gt;func = [&amp;p_a]() { p_a = /* something else */ };,它可以防止递归,这会在设置内部指针和破坏对象时发生,反之亦然。
  • 是的,它可以防止从被引用对象的析构函数中调用的任何隐式或显式重置。
  • 是的,当reset 抛出异常(从删除器)然后调用unique_ptr 析构函数时,它可能会阻止递归。
【解决方案2】:

这只是一条评论,但太长了,无法放入评论部分。所以我将其发布为答案。

首先让我们看一下标准,它说

u 可以根据请求将所有权转移到另一个唯一指针u2。完成后 这样的转移,以下后置条件成立:
(4.1)——u2.p等于预转u.p,
(4.2) — u.p 等于 nullptr,并且
(4.3)——如果u.d保持前转移状态,该状态已经转移到u2.d
与重置的情况一样,u2 必须通过预转让正确处置其预转让拥有的对象 在所有权转移被视为完成之前关联的删除器。

标准仅指定了重置后的行为,重置期间的确切行为取决于实现。

然后我们看一下 GNU 的实现:

void reset(pointer __p) noexcept
{
  const pointer __old_p = _M_ptr();
  _M_ptr() = __p;
  if (__old_p)
    _M_deleter()(__old_p);
}
~unique_ptr() noexcept
{
  static_assert(...);
  auto& __ptr = _M_t._M_ptr();
  if (__ptr != nullptr)
    get_deleter()(std::move(__ptr));
  __ptr = pointer();
}

这看起来很奇怪,因为reset 和 dtor 中的确切顺序不同。在我阅读 MSVC 实现之前,这对我来说没有意义:

void reset(pointer _Ptr = nullptr) noexcept {
    pointer _Old = _STD exchange(_Mypair._Myval2, _Ptr);
    if (_Old) {
        _Mypair._Get_first()(_Old);
    }
}

reset 时,当然我们需要用参数交换当前指针。另外,不要忘记调用删除器。简单的工作。

~unique_ptr() noexcept {
    if (_Mypair._Myval2) {
        _Mypair._Get_first()(_Mypair._Myval2);
    }
}

在析构函数中,微软不会费心将指针设置为nullptr,他们只是调用删除器。很简单。

如果你真的想将指针设置为nullptr,我敢打赌你会在调用删除器后设置它。


结论:

我不认为 STL 作者真的想“阻止”你做某事。他们只是简单地编写最直接的代码来实现需求。

【讨论】:

  • reset()~unique_ptr() 在 GNU 标准库中是不同的,因为析构函数不能被隐式递归调用,reset 可以。
  • @S.M. class A { unique_ptr&lt;A&gt; a; A() { a.reset(this); } }
  • 然后呢?代码错误,循环依赖。 A 的析构函数调用了两次,与 delete a 两次的问题相同。
  • "这只是一条评论,但是太长了,无法放入评论区。" -- 如果评论太长而无法放入评论区,则不符合 SO 的 cmets 指南,因此不应发布。但是,在这种情况下,您可能会认为它“只是”一条评论是错误的。它至少非常接近答案。
猜你喜欢
  • 2014-09-17
  • 1970-01-01
  • 2020-08-19
  • 2017-05-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多