【问题标题】:std::unique_ptr reset() order of operationsstd::unique_ptr reset() 操作顺序
【发布时间】:2021-09-01 00:40:27
【问题描述】:

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

给定 current_ptr,由 *this 管理的指针按此顺序执行以下操作:

  1. 保存当前指针 old_ptr = current_ptr 的副本
  2. 用参数 current_ptr = ptr 覆盖当前指针
  3. 如果旧指针非空,则删除之前管理的对象 if(old_ptr) get_deleter()(old_ptr)。

cppreference

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

[...] [ 注意:这些操作的顺序很重要,因为调用 get_deleter() 可能会破坏 *this。 ——尾注]

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

【问题讨论】:

  • 说这个unique_ptr指向某个对象A,又是某个对象B的数据成员。 A 的析构函数可能会删除 B,这反过来又会破坏指针。你希望它在那个时候为空,否则你最终会遭受双重破坏。
  • 如果有析构函数抛出,那么这个命令也更安全(不好的做法)
  • @Phil1970 析构函数不能抛出。
  • @prehistoricpenguin 这是个坏主意,语言默认为 noexcept 析构函数,但可以做到。
  • @IgorTandetnik 您能否从实践中指定一个示例,其中A 的析构函数将删除B?我自己也无法想象。

标签: c++ unique-ptr


【解决方案1】:

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

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

std::shared_ptr 不同,std::unique_ptr 可以通过任何满足 NullablePointer 的自定义句柄类型来管理对象。例如,这允许通过提供定义typedef boost::offset_ptr pointerDeleter 来管理位于共享内存中的对象;或其他花哨的指针。

所以,按照目前的顺序:

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

    如果花式指针的复制构造函数抛出,unique_ptr 仍然拥有原始对象而新对象未被拥有:OK

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

    如果花式指针的复制赋值抛出,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 覆盖当前指针

    如果花式指针的复制赋值抛出,我们的 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 甚至无法安全销毁,因为对其内部指针调用删除器的结果可能是双释放。


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

笔记说

...对get_­deleter() 的调用可能会破坏*this

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

虽然这种极端情况是将删除器调用放在最后的一个充分理由,但我觉得强异常安全参数可能不那么做作(尽管具有指向自身的唯一指针的对象是否比具有投掷任务是任何人的猜测)。

【讨论】:

    【解决方案2】:

    std::enable_shared_from_this 类似,您可以创建使用std::unique_ptr 来管理自己的生命周期的对象,而无需std::shared_ptr 的开销,只要您只需要std::unique_ptr。在下面的示例中,我们创建了一个“一劳永逸”的任务,它在完成工作后会自毁。动机可以在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。注意“销毁”。只打印一次。

    为什么不先执行 3),然后执行 2)?

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

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

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

    我不知道委员会是否因为这个用例决定指定reset

    注意事项:

    【讨论】:

    • 在这种技术中使用std::unique_ptr 是否应该是对new/delete 的改进?如果是的话,怎么办?
    • 我没这么说。很难找到好处:通常的答案是std::unique_ptr 避免泄漏,尤其是在遇到异常时。但是在CreateTask 中使用new 不会导致泄漏,因为以下操作都不会抛出。之后,std::unique_ptr 将无法在其末尾删除Task。所以我认为只有在简单地禁止原始new/delete 的代码库中才会有好处。通常必须遵循isocpp.github.io/CppCoreGuidelines/…
    • 虽然该示例是人为设计的,但如果每个循环都存在A --&gt; B --&gt; C --&gt; D --&gt; A,则避免无限循环将是一个很好的理由,因为重置循环的任何项目都会破坏它们。
    猜你喜欢
    • 2021-05-30
    • 2015-04-14
    • 2017-02-24
    • 2012-12-01
    • 1970-01-01
    • 2018-07-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多