std::unique_ptr reset() 操作顺序

std::unique_ptr reset() order of operations

调用 void reset( pointer ptr = pointer() ) noexcept; 调用以下操作

Given current_ptr, the pointer that was managed by *this, performs the following actions, in this order:

  1. Saves a copy of the current pointer old_ptr = current_ptr
  2. Overwrites the current pointer with the argument current_ptr = ptr
  3. If the old pointer was non-empty, deletes the previously managed object if(old_ptr) get_deleter()(old_ptr).

cppreference

这个特定订单的原因是什么?为什么不只做 3) 然后 2)?在这个问题 std::unique_ptr::reset checks for managed pointer nullity? 中,第一个答案引用了标准

[…] [ Note: The order of these operations is significant because the call to get_deleter() may destroy *this. —end note ]

这是唯一的原因吗? get_deleter() 怎么会毁掉 unique_ptr (*this)?

类似于std::enable_shared_from_this you can create objects that use std::unique_ptr to manage their own lifetime, without the overhead of std::shared_ptr whenever a std::unique_ptr is all you need. In the example below we create a "fire and forget" Task that self-destructs after doing its work. Motivation can be found here

#include <cstdio>
#include <future>
#include <memory>
#include <thread>

using namespace std::chrono_literals;

class Task
{
  public:
    Task(Task&&) = delete;
    Task& operator=(Task&&) = delete;
    Task(const Task&) = delete;
    Task& operator=(const Task&) = delete;
    ~Task() { std::printf("Destroyed.\n"); }

    static Task* CreateTask()
    {
        // Can't use std::make_unique because the constructor is private.
        std::unique_ptr<Task> task{new Task{}};
        Task* const result{task.get()};
        result->AcceptOwnershipOfSelf(std::move(task));
        return result;
    }

    void Run()
    {
        // Do work ...

        // Work is done. Self-destruct.
        self_.reset();
    }

  private:
    // Constructor needs to be private: Task must be created via `CreateTask`.
    Task() = default;
    void AcceptOwnershipOfSelf(std::unique_ptr<Task> self) { self_ = std::move(self); }

    std::unique_ptr<Task> self_;
};

int main()
{
    Task* const task{Task::CreateTask()};
    std::ignore = std::async(&Task::Run, task);
    std::this_thread::sleep_for(1s);
}

godbolt。注意是“销毁”。只打印一次。

Why not just do 3) and then 2)?

如果 self_.reset(); 在清零 current_ptr 之前删除之前管理的指针,self_ 仍将指向 ~Task 中的 *this - 导致无限循环。我们可以通过将 self_.reset(); 替换为 self_->~Task();.

来看到

当然我们可以用下面的两行替换self_.reset();

        std::unique_ptr<Task> tmp{std::move(self_)};
        tmp.reset();

我不知道委员会是否因为这个用例而决定像他们那样指定 reset

备注:

  • Deleting this 本身并没有被禁止。
  • This stack overflow answer 建议使用原始 new/delete 而不是 std::unique_ptr.
  • 的类似方法

在分析规定的步骤顺序时,它通常很有用,可以考虑哪些步骤可能会抛出以及什么状态会让一切都处于 - 目的是我们永远不会陷入无法恢复的境地。

请注意,从文档 here 中可以看出:

Unlike std::shared_ptr, std::unique_ptr may manage an object through any custom handle type that satisfies NullablePointer. This allows, for example, managing objects located in shared memory, by supplying a Deleter that defines typedef boost::offset_ptr pointer; or another fancy pointer.

因此,在当前顺序中:

  1. 保存当前指针的副本old_ptr = current_ptr

    如果 fancy pointer 的复制构造函数抛出异常,unique_ptr 仍然拥有原始对象并且新对象未被拥有:OK

  2. 用参数覆盖当前指针 current_ptr = ptr

    如果 fancy pointer 的复制赋值抛出,unique_ptr 仍然拥有原始对象并且新对象不被拥有:OK

    (假设奇特指针的复制赋值运算符满足通常的异常安全保证,但如果没有它,unique_ptr 将无能为力)

  3. 如果旧指针非空,则删除先前管理的对象 if(old_ptr) get_deleter()(old_ptr)

    在这个阶段 unique_ptr 拥有新对象,删除旧对象是安全的。

换句话说,两种可能的结果是:

std::unique_ptr<T, FancyDeleter> p = original_value();
try {
  auto tmp = new_contents();
  p.reset(tmp);
  // success
}
catch (...) {
  // p is unchanged, and I'm responsible for cleaning up tmp
}

在您建议的订单中:

  1. 若原指针非空,则删除

    现阶段unique_ptr无效:已经提交了不可逆的更改(删除),如果下一步失败就无法恢复良好状态

  2. 用参数覆盖当前指针 current_ptr = ptr

    如果 fancy pointer 的复制赋值抛出异常,我们的 unique_ptr 将变得不可用:存储的指针是不确定的,我们无法恢复旧指针

换句话说,我所说的无法恢复的情况如下所示:

std::unique_ptr<T, FancyDeleter> p = original_value();
try {
  auto tmp = new_contents();
  p.reset(tmp);
  // success
}
catch (...) {
  // I can clean up tmp, but can't do anything to fix p
}

在那次异常之后,p甚至无法安全销毁,因为在其内部指针上调用删除器的结果可能是双重释放。


注意。删除器本身不允许抛出,所以我们不必担心。

留言说

... the call to get_­deleter() might destroy *this.

听起来不对,但是调用get_­deleter()(old_­p)确实可能 ...如果*old_p是一个对象包含一个 unique_ptr 给自己。在这种情况下,删除器调用必须放在最后,因为实际上您无法安全地对它之后的 unique_ptr 实例执行任何操作。

尽管这种特殊情况是将删除器调用放在最后的可靠理由,但我觉得强异常安全论点可能不那么做作(尽管具有指向自身的唯一指针的对象是否比幻想更常见或更不常见带有抛出赋值的指针是任何人的猜测)。