【问题标题】:Does std::unique_ptr set its underlying pointer to nullptr inside its destructor?std::unique_ptr 是否在其析构函数内将其底层指针设置为 nullptr?
【发布时间】:2019-01-17 13:36:08
【问题描述】:

在实现我自己的unique_ptr(只是为了好玩)时,我发现它无法从libstdcxx 传递这个test file

struct A;

struct B
{
  std::unique_ptr<A> a;
};

struct A
{
  B* b;
  ~A() { VERIFY(b->a != nullptr); }
};

void test01()
{
  B b;
  b.a.reset(new A);
  b.a->b = &b;
}

gcc passes这个测试文件很开心(当然这个文件来自libstdcxx),而clang fails则是VERIFY部分。

问题:

  1. 它是依赖于实现还是未定义的行为?
  2. 我猜这个后置条件 (b-&gt;a != nullptr) 对 gcc 很重要,否则它不会有它的测试文件,但我不知道它背后是什么。跟优化有关系吗?我知道很多 UB 都是为了更好的优化。

【问题讨论】:

  • 很高兴看到您的 unique_ptr 实现以供参考(至少是析构函数)。
  • @user673679 我认为 OP 所说的行为来自标准 unique_ptr(请参阅 Wandbox 链接)。
  • 我似乎无法在任何地方重现这种行为。它通过coliru
  • 是的,问题是关于 std::unique_ptr,不是我自己的
  • @MárioFeroldi clang 在 coliru 上使用来自 GCC 的 libstdc++。添加-stdlib=libc++,你会得到一个断言失败。

标签: c++ unique-ptr nullptr


【解决方案1】:

clang (libc++) 在这一点上似乎不合规,因为标准说:

[unique.ptr.single.dtor]

~unique_ptr();
  1. 要求: 表达式get_­deleter()(get()) 应具有良好的格式,应具有良好定义的行为,并且不应引发异常。 [ 注意:default_­delete 的使用要求T 是一个完整的类型。 — 尾注 ]

  2. 效果:如果get() == nullptr 没有效果。 否则get_­deleter()(get())

所以析构函数应该等价于get_deleter()(get()),这意味着b-&gt;a不能是A的析构函数中的nullptr(在get_deleter()内部由delete指令调用)。


附带说明,clang (libc++) 和 gcc (libstdc++) 在销毁 std::unique_ptr 时都将指针设置为 nullptr,但这里是 gcc 析构函数:

auto& __ptr = _M_t._M_ptr();
if (__ptr != nullptr)
    get_deleter()(__ptr);
__ptr = pointer();

...这里是clang(致电reset()):

pointer __tmp = __ptr_.first();
__ptr_.first() = pointer();
if (__tmp)
   __ptr_.second()(__tmp);

如您所见,gcc 首先删除然后分配给nullptr (pointer()),而clang 首先分配给nullptr (pointer()) 然后删除1


1pointer 是对应于Deleter::pointer 的别名,如果存在的话,或者只是T*

【讨论】:

  • 离题:pointer 只是默认为Deleter::pointer,否则回退到T *
  • @user673679 我认为你是对的。标准没有指定后置条件,但get_deleter()(get())clang 的行为不一致。我会更新答案。
  • VERIFY 函数尝试在 unique_ptr 的析构函数启动后访问 unique_ptr 的实例。这不是未定义的行为吗?
  • @Handy999 这不是 UB (AFAIK),您可以调用成员函数(注意虚函数)并访问被销毁对象的成员。参见例如stackoverflow.com/questions/10979250/….
  • 我得回归正题了。来自 N4659:>>20.5.4.10 库对象访问 2 如果访问标准库类型的对象,并且对象的生命周期(6.8)的开始没有发生在访问之前,或者访问没有发生在结束之前对象的生命周期,除非另有说明,否则行为未定义。>6.8 对象生命周期 (1.2) ... T 类型对象的生命周期在以下情况下结束: ... T 是具有非平凡的类类型析构函数(15.4),析构函数调用开始,
【解决方案2】:

libstdc++ 和 libc++ 都符合要求,因为定义明确的程序无法观察到这一点。在析构函数执行期间,[res.on.objects]/2 禁止任何尝试观察(或修改)unique_ptr 的状态,以免造成未定义行为:

如果访问标准库类型的对象,并且对象生命周期的开始没有发生在访问之前,或者访问没有发生在对象的生命周期结束之前,除非另有说明,否则行为是未定义的。

事实上,unique_ptr 的析构函数首先添加这一段的原因(LWG2224)。

另外,销毁完成后,其占用的存储内容由[basic.life]/4不确定:

本文档中赋予对象和引用的属性仅在其生命周期内适用于给定对象或引用。

【讨论】:

    【解决方案3】:

    std::unique_ptr&lt;&gt;销毁后占用内存的最终状态没有要求。将其设置为 null 是没有意义的,因为内存正在返回到分配它的位置。 GCC 可能会检查它是否不为空,以确保没有人添加不必要的代码来清除它。在适当的情况下,在不需要时强制清除该值可能会导致性能下降。

    【讨论】:

      猜你喜欢
      • 2015-04-29
      • 2019-12-13
      • 1970-01-01
      • 1970-01-01
      • 2015-10-15
      • 1970-01-01
      • 1970-01-01
      • 2012-04-15
      • 1970-01-01
      相关资源
      最近更新 更多