【问题标题】:unique_ptr: linked list entry deletionunique_ptr:链表条目删除
【发布时间】:2023-04-01 18:35:01
【问题描述】:

我目前正在考虑借助 unique_ptrs 实现单链表。尽管由于析构函数的递归调用可能导致堆栈溢出问题(请参阅Stack overflow with unique_ptr linked list),但我遇到了以下问题: 假设,我们有如下链表的实现

struct node {
  node (void) : val(0), next(nullptr) {}
  int val;
  std::unique_ptr<node> next;
};

我们已经根据

初始化了我们的列表
int main (int argc, char* argv[]) {
  node HEAD;
  HEAD.val = 0;
  auto ptr = &HEAD;
  for (int i = 0; i < 10; ++i) {
    ptr->val = i;
    ptr->next.reset(new node);
    ptr = ptr->next.get();
  }
  ptr->val = 10;
  ...

现在,我想删除值为 1 的节点:

ptr = &HEAD;
ptr = ptr->next.get();
HEAD.next = std::move(ptr->next);

乍一看,这似乎是明智的。不过,我不确定它是否会导致未定义的行为:

根据http://en.cppreference.com/w/cpp/memory/unique_ptr/operator%3D, 运算符=

将所有权从 r 转移到 *this,就好像通过调用 reset(r.release()) 后跟从 std::forward 分配 get_deleter() (r.get_deleter())

仔细查看 unique_ptr::reset (http://en.cppreference.com/w/cpp/memory/unique_ptr/reset),它显示

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

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

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

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

这意味着在这种情况下调用 r.get_deleter() 将引用一个已经被销毁的对象,还是我在这里弄错了?

非常感谢您的回复。

【问题讨论】:

    标签: c++ linked-list undefined-behavior unique-ptr


    【解决方案1】:

    您的担忧对我来说似乎是正确的。 ptr->next.get_deleter() 不再存在,因为当ptr 被重置时,ptr-&gt;next*ptr 一起被销毁(尽管不再拥有任何东西)。在大多数情况下,这可能无关紧要,因为删除器将是一个空的基类,而赋值将是一个空操作,但在技术上仍然是未定义的行为。

    我会明确选择HEAD.next.reset(ptr-&gt;next.release());(如果您不关心删除器),或者如果您担心保留删除器,我会选择HEAD.next = std::unique_ptr&lt;node&gt;(std::move(ptr-&gt;next))

    【讨论】:

    • 赞成,但这种方法的一个潜在问题是与第二个节点关联的删除器已丢失。如果您没有有状态的删除器,那么这不是问题,但我怀疑 OPs 方法无论如何都适用于这种情况。
    【解决方案2】:

    我认为你是对的。

    唯一阻止HEAD.next-&gt;next unique_ptr 被销毁的是HEAD.next node 持有它。分配给HEAD.next 的行为打破了 试图从HEAD.next-&gt;next unique_ptr 获取删除器之前。

    对我来说,删除值为 1 的节点的直观方法是将尾部与头部分离,然后从值为 2 的节点重新附加尾部:

    auto tail = std::move(HEAD.next);
    HEAD.next = std::move(tail.next);
    

    你想要做的就像切割和重新连接一条蛇,只用一只手握住蛇,如果你不小心,蛇的碎片可能会蠕动。

    编辑:我尝试使用这样的有状态删除器:

    #include <memory>
    #include <iostream>
    
    struct node;
    
    class NodeDeleter {
      int deleterNumber;
    public:
      NodeDeleter() : deleterNumber(-1) {}
      NodeDeleter(int deleterNumber) : deleterNumber(deleterNumber) {}
      void operator()(node* n);
    };
    
    struct node {
      node() : val(0), next(nullptr) {}
      int val;
      std::unique_ptr<node, NodeDeleter> next;
    };
    
    void NodeDeleter::operator()(node* n) {
      std::cout << "Deleting node with value " << n->val << " using deleter " << deleterNumber << "\n";
      delete n;
    }
    
    std::unique_ptr<node, NodeDeleter> make_node(int deleterNumber) {
      return std::unique_ptr<node, NodeDeleter>(new node(), NodeDeleter(deleterNumber));
    }
    
    int main() {
      node HEAD;
      HEAD.val = 0;
      auto ptr = &HEAD;
      for (int i = 0; i < 10; ++i) {
        ptr->val = i;
        ptr->next = make_node(i);
        ptr = ptr->next.get();
      }
      ptr->val = 10;
    
      HEAD.next = std::move(HEAD.next->next);
    }
    

    有趣的是,在 GCC 中它似乎表现自己,但在 Visual Studio 2015 中,它显然是在调用未定义的行为。输出是:

    Deleting node with value 1 using deleter 0
    Deleting node with value 2 using deleter -572662307
    Deleting node with value 3 using deleter 2
    Deleting node with value 4 using deleter 3
    ...
    

    它看起来像是在销毁后尝试使用删除器 1。

    【讨论】:

    • 我认为这以及@JosephIreland 的回答可能会解决问题。特别是,我同意你切割和重新连接蛇的形象:)
    • 您是否尝试在 NodeDeleter 析构函数中添加打印?这很有趣(我正在使用 gcc 顺便说一句)。
    • @FranMowinckel 我做了,但是有太多的构造、复制和分配删除器正在进行,有很多对析构函数的虚假调用 (melpon.org/wandbox/permlink/5fl4EKiednA6m0rU)。我想如果你跟踪它们,你可以看到它在使用它之前删除了最后一个副本。
    • @ChrisDrew 我的意思是最后,节点被按顺序删除,但删除器被按相反的顺序删除并且在节点被删除之后。我觉得很有趣。顺便说一句,我也在 VC++ 中进行了测试,但取决于它是否有效。
    【解决方案3】:

    我认为您弄错了,因为当您移动指针 (std::move) 时,您正在生成一个右值并将ptr-&gt;next 留在 已发布 状态所以这不是一个未定义的行为。

    我的意思是,在 reset 之前,您要释放指针:reset(r.release())

    然后:

    HEAD.next = std::move(HEAD.next->next);
    

    在 cmets 之后编辑(只是添加我正在测试的代码):

    struct custom_deleter {  // a deleter class with state
      custom_deleter() {}
      ~custom_deleter() {
        std::cout << "custom deleter deleted" << std::endl;
     }
      template <class T>
      void operator()(T* p) {
        std::cout << "custom deleter()" << std::endl;
        delete p;
      }
    };
    
    struct node {
      node (void) : val(0), next(nullptr) {}
      ~node () {
        std::cout << "~node " << val << std::endl;
      }
      int val;
      std::unique_ptr<node, custom_deleter> next;
    };
    
    int main (int argc, char* argv[]) {
      node HEAD;
      HEAD.val = 0;
      auto ptr = &HEAD;
      for (int i = 0; i < 10; ++i) {
        ptr->val = i;
        ptr->next.reset(new node);
        ptr = ptr->next.get();
      }
      ptr->val = 10;
    
      std::cout << "And now we delete it" << std::endl;
      HEAD.next = std::move(HEAD.next->next);
    
      std::cout << "Looping" << std::endl;
      ptr = &HEAD;
      for (int i=0; i < 9; ++i) {
          std::cout << ptr->val << std::endl;
          ptr = ptr->next.get();
      }
      std::cout << "End." << std::endl;
    }
    

    结果:

    custom deleter()
    ~node 1
    custom deleter deleted
    Looping
    0
    2
    3
    4
    5
    6
    7
    8
    9
    End.
    ~node 0
    custom deleter()
    ~node 2
    custom deleter()
    ~node 3
    custom deleter()
    ~node 4
    custom deleter()
    ~node 5
    custom deleter()
    ~node 6
    custom deleter()
    ~node 7
    custom deleter()
    ~node 8
    custom deleter()
    ~node 9
    custom deleter()
    ~node 10
    custom deleter deleted
    custom deleter deleted
    custom deleter deleted
    custom deleter deleted
    custom deleter deleted
    custom deleter deleted
    custom deleter deleted
    custom deleter deleted
    custom deleter deleted
    custom deleter deleted
    

    【讨论】:

    • 我认为关键是,防止unique_ptr HEAD.next-&gt;next 被破坏的唯一因素是HEAD.next 持有它。分配给HEAD.next 的行为打破了试图从HEAD.next-&gt;next 获取删除器之前。
    • @ChrisDrew HEAD.next-&gt;next 确实被销毁了,但它之前发布过,所以它的 content 不是。我已经(快速)测试了代码,它工作正常。
    • OP担心的不是unique_ptr的内容。它是unique_ptr中的删除器。
    • @ChrisDrew 仍然没有被破坏。我认为删除器是其内容的一部分。我会更新我的答案以显示它。
    • 查看我的答案,我已经尝试使用有状态删除器,并且确实可以在 VS2015 上调用未定义的行为。
    猜你喜欢
    • 2019-01-18
    • 1970-01-01
    • 1970-01-01
    • 2021-12-09
    • 2011-06-05
    • 2013-06-27
    • 2016-05-29
    • 2021-06-01
    相关资源
    最近更新 更多