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

Wut*_*utz 23 c++ unique-ptr language-lawyer move-semantics

我对此有一个后续问题: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);
    }
 
    ...
};
Run Code Online (Sandbox Code Playgroud)

答案告诉我,析构函数中的循环就所包含的原始指针而言是明确定义的,因为unique_ptr::operator=(unique_ptr&& other)被定义为 call ,这保证了在删除所持有的 raw_pointer 之前reset(other.release())提取原始指针。otherthis

我相信 cppreference 上的代码仍然是错误的,因为删除器在调用删除我们正在使用的 unique_ptr whos 引用后被传输。reset为了澄清这一点,根据我的理解,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
}
Run Code Online (Sandbox Code Playgroud)

我的推理正确吗?这实际上是 UB 并且 cppref 上的代码是错误的吗?或者这可以吗,因为使用的 unique_ptr 没有存储默认删除器?

use*_*522 19

据我所知你是正确的。当前的标准草案指定了单对象std::unique_ptr的移动分配,如下所示,请参阅[unique.ptr.single.asgn]/3

\n
\n

constexpr unique_ptr& operator=(unique_ptr&& u) noexcept;

\n

[...]

\n

效果:reset(u.release())调用后接get_deleter() = std\xe2\x80\x8b::\xe2\x80\x8bforward<D>(u.get_deleter()).

\n
\n

您是正确的,有可能会间接结束链表示例中的reset生命周期。u然后,u.get_deleter()无论删除器的类型如何,总是具有未定义的行为,因为您无法在对象的生命周期之外调用非静态成员函数。托管指针u之前被释放u.release()根本不重要。

\n

因此head = std::move(head->next);在链表示例中具有未定义的行为。

\n

看起来这确实是所有三大标准库实现的实现方式,可以使用 C++23 进行验证,其中std::unique_ptr可用constexpr。该程序

\n
#include<memory>\n\nstruct A {\n    std::unique_ptr<A> a;\n};\n\nconsteval void f() {\n    auto a = std::make_unique<A>(std::make_unique<A>());\n    a = std::move(a->a);\n}\n\nint main() {\n    f();\n}\n
Run Code Online (Sandbox Code Playgroud)\n

由于在常量表达式中访问超出其生命周期的对象,无法使用 Clang libstdc++、Clang libc++ 和 MSVC 进行编译。(GCC似乎对常量表达式中的UB比较宽松,是否检测库函数中的UB未明确,所以可以。)

\n

我可能误解了标准中“效果: ”的解释方式,但这对我来说似乎是一个缺陷。这种行为是非常出乎意料的。我认为应该可以以一种有效的方式指定这一点,例如u首先移动到临时副本,然后按照当前规则进行交换或移动分配。这也可以用作解决方法(此处使用 C++23auto语法):

\n
head = auto(std::move(ahead->next));\n
Run Code Online (Sandbox Code Playgroud)\n

或者,如果我误解了“ Effects: ”子句的含义,并且它要求实现没有 UB,即使u会通过 删除reset,那么所有三个主要实现的行为都不符合要求。

\n

不过,我在这个问题上找不到任何公开的 LWG 问题。

\n