移动唯一指针 - cppreference 上的未定义行为?

问题描述 投票:0回答:2

我有一个后续问题:Move unique_ptr: reset the source vs. destroy the old object

对于原始问题的快速总结,cppreference 上有此示例代码:

struct List
{
    struct Node
    {
        int data;
        std::unique_ptr<Node> next;
    };
 
    std::unique_ptr<Node> head;
 
    ~List()
    {
        // destroy list nodes sequentially in a loop, the default destructor
        // would have invoked its `next`'s destructor recursively, which would
        // cause stack overflow for sufficiently large lists.
        while (head)
            head = std::move(head->next);
    }
 
    ...
};

答案告诉我,就包含的原始指针而言,析构函数中的循环定义明确,因为

unique_ptr::operator=(unique_ptr&& other)
被定义为调用
reset(other.release())
,这保证了原始指针是在 raw_pointer 之前从
other
中提取的
this
持有的被删除。

我相信 cppreference 上的代码仍然是错误的,因为删除器在调用reset

 删除我们正在使用的 unique_ptr whos 引用后被转移。澄清一下,我理解的 operator= 的注释实现:

auto& unique_ptr<T, Deleter>::operator=(unique_ptr<T, Deleter>&& other) { // Here, "other" refers to a unique_ptr that lives in the T that is owned by our instance // Thus, if we delete the raw pointer held in this instance, we delete the object that "other" references reset(other.release()); // This correctly transfers over the raw pointer from "other", but deletes the raw pointer held in this instance, thus deleting the object "other" refers to get_deleter() = std::forward<Deleter>(other.get_deleter()); // this should be UB, because "other" is a dangling reference }
我的推理正确吗?这实际上是 UB 并且 cppref 上的代码是错误的吗?或者这可以吗,因为使用的 unique_ptr 没有存储默认删除器?

c++ unique-ptr move-semantics
2个回答
1
投票
据我所知,你是正确的。目前的标准草案是这样规定单体

std::unique_ptr

的移动分配的,见
[unique.ptr.single.asgn]/3:

效果: 调用 reset(u.release()),然后调用 get_deleter() = std :: forward(u.get_deleter())。

您是正确的,

reset

 可能会间接结束 
u
 的生命周期,就像链表示例中的情况一样。那么 
u.get_deleter()
 总是有未定义的行为,不管删除器的类型如何,因为你不能在对象的生命周期之外调用非静态成员函数。托管指针是由 
u
 早些时候从 
u.release()
 释放的根本无关紧要。

因此链表示例中的

head = std::move(head->next);

具有未定义的行为。

这似乎确实是所有三大标准库实现实现它的方式,可以使用 C++23 进行验证,其中

std::unique_ptr

constexpr
-可用的。程序

#include<memory> struct A { std::unique_ptr<A> a; }; consteval void f() { auto a = std::make_unique<A>(std::make_unique<A>()); a = std::move(a->a); } int main() { f(); }

无法使用 Clang libstdc++、Clang libc++ 和 MSVC 编译。 (GCC似乎对常量表达式中的UB比较宽容。是否检测库函数中的UB是未指定的,所以没关系。)

我可能误解了标准中应该如何解释“

Effects:”,但这对我来说似乎是一个缺陷。这种行为非常出乎意料。我认为应该可以用一种可行的方式来指定它,例如首先将 u

 移动到临时副本,然后按当前规则交换或移动分配。这也可以用作解决方法(这里使用 C++23 
auto
 语法):

head = auto(std::move(ahead->next));
或者如果我

am误解了“Effects:”子句的含义并且它要求实现没有UB,即使u

会通过
reset
删除,那么所有三个主要实现的行为都不符合。 

不过,我找不到关于此事的任何未解决的 LWG 问题。


0
投票
恕我直言,这是 std 在未来的审查中必须解决的问题之一。让我们内联析构函数循环看看发生了什么:

while(head){ head.reset(head->next.release()); head.get_deleter() = std::forward<Deleter> (head->next.get_deleter()); };
一旦

head

reset
Deketer
中推断的
head->next
实例已经默认构造。只要 
Deleter
 是单态的并且可以简单地移动分配 - 
std::default_deleter
 就是这种情况 - 下一条指令是 
NOOP
 ,这是明确定义的。但是如果 
Deleter
 不是单态的(即 
Deleter
 的实例带有不同的值)或者不是平凡的移动分配 - 由于 
head->next.get_deleter()
 的先前值的丢失 - 将有一个可能的 UB。这个 UB 与
Deleter
 上的自我分配测试无关。由于 
Deleter
 正在管理推断指针的销毁,它实际上表现得像大多数其他 STL 类中的分配器。因此,
std::unique_ptr
 的正确实现将在推断指针之前构造和分配 
Deleter
。这个问题可能是标准缺陷报告的主题,或者至少是进一步的标准澄清恕我直言。

© www.soinside.com 2019 - 2024. All rights reserved.