【问题标题】:Avoid infinite recursion in destructor避免析构函数中的无限递归
【发布时间】:2021-07-31 23:04:54
【问题描述】:

作为我的大学要求我完成的一项练习的一部分,我编写了一个小型 Graph 实现,遵循此标题。

class Node {

private:
    std::string name;
    std::vector<Node*> children;

public:
    Node(const std::string& name="");
    virtual ~Node();

}

在为析构函数~Node() 编写代码时,我注意到当图形包含循环时我的实现会失败。这是我目前的实现,如果图表包含循环,这显然不起作用。

Node::~Node() {
    for (Node* n : children) {
        delete n;
        n = NULL;
    }
    children.clear();
}

我不确定如何最优雅地编写一个可以处理图中循环的析构函数?

请注意,我专门负责编写递归析构函数。 感谢您的回答!

【问题讨论】:

  • 1.不要删除~Node 中的children。 2.让更高级别的函数/类来管理图中所有Nodes的销毁。
  • 您的所有权似乎存在问题。您拥有共享所有权(多个对象可以具有指向同一个对象的指针,并且它们中的任何一个都可能负责deleteing)以及潜在的循环所有权。这不是一个小问题。您应该更改您的所有权方案,将您的Nodes 存储在某种Graph 对象中,并且只有Graph 对象负责销毁它包含的每个Node。当指针拥有对象时,请记住使用 unique_ptr 而不是原始指针(std::unique_ptr&lt;T&gt; 与 T*)。
  • “请注意,我专门负责编写递归析构函数。”递归析构函数对于这类问题来说是一个非常糟糕的解决方案。
  • @FrançoisAndrieux “对于这类问题,递归析构函数是一个非常非常糟糕的解决方案”,如果问题是教所有权和递归析构函数的危险,则不是。大学是试验代码和理解选择的安全场所。你现在想解决这些问题,而不是在你有真正责任和真正后果的工作上。
  • @bolov 这仍然是一个糟糕的解决方案,即使它是一个糟糕的解决方案的好例子。 OP 现在知道这一点很好,以防他们不会在糟糕的解决方案不会产生实际后果的大学中学习它。

标签: c++ recursion graph c++14 destructor


【解决方案1】:

选项 1:为图选择一种表示,其中节点不属于其他节点,而是选择作为不同对象的图。这样节点析构函数就不需要做任何事情了。 这不满足递归的要求:

struct Graph {
    std::vector<std::unique_ptr<Node>> nodes;
};

请注意,如果不涉及继承,那么您可以简单地使用std::vector&lt;Node&gt;。我假设有,由于在Node 中使用了虚拟破坏器。

或者,您可以为图形使用另一种表示形式,例如邻接列表。

选项 2:使用算法生成图的最小生成森林。然后递归删除每个生成树的根。例如,您可以使用 Kruskal 算法。 (根据您的表示,您的图表看起来可能是连接的,在这种情况下将只有一棵生成树)。

【讨论】:

  • 选项三:以任意顺序收集链表中的节点,完全不考虑图边。然后递归释放列表。
  • @n.'pronouns'm。或者也许将它们收集在一个集合中,这样我们就不需要另一个数据结构来检测是否需要在循环的情况下停止收集。此外,使用平衡的 BST,递归实际上是有意义的。
  • 就我而言,学生可以使用递归在程序结束时打印“完成”消息,这与其他任何事情一样有意义。没有,就是这样。为了使解决方案有意义,问题需要有意义,而事实并非如此。免责声明,我不对负责为解决方案评分的人的意见负责。
【解决方案2】:

一种选择可能是首先创建所有Node*s 中的unordered_set,然后再创建delete。

void fill(std::unordered_set<Node*>& to_delete, Node* ptr) {
    // try to insert ptr and return if it was already in the set
    if(not to_delete.emplace(ptr).second) return;

    // swap ptr->children with an empty vector
    std::vector<Node*> tmp;
    std::swap(tmp, ptr->children);

    for(Node* c : tmp)       // loop over the pointers
        fill(to_delete, c);  // fill recursively
}

virtual ~Node() {
    if(children.empty()) return;          // nothing to do here

    std::unordered_set<Node*> to_delete;  // to collect all the Node*'s
    fill(to_delete, this);                // fill the set recursively
    to_delete.erase(this);                // don't delete "this"

    for(auto c : to_delete)               // delete all - they have no children by now
        delete c;
}

Demo

【讨论】:

  • 旁注:_ 通常用作gettext 的宏,这是一个使用非常广泛的库。因此,在使用此类库的大型程序中,将其用作变量名可能会出现问题。
  • @eerorika 哦,我会替换它。我想当我写这篇文章时,我被一点 Python 打动了 :) 谢谢。
  • 您可以使用[[maybe_unused]] 来传达相同的意图。
  • @eerorika 是的。我改为使用 C++14 兼容版本。我不记得在回答时看到问题上的 C++14 标记 - 但现在它就在那里。
【解决方案3】:

如果您的图是一棵树(我假设它是因为您的析构函数的实现仅对树有效)并且您可以存储 Node 的父级,那么您可以编写不需要任何额外数据结构的迭代版本避免递归。

还要学习使用智能指针。

class Node {

private:
    std::string name;
    std::vector<std::unique_ptr<Node>> children;
    Node* parent;

    void safeCleanClildren();
public:
    Node(std::string name="", Node* parent = nullptr)
        : name{std::move(name)}
    {}

    ~Node() {
       iterativeCleanClildren();
    }

    void addChild(std::string name) {
        children.emplace_back(std::make_unique<Node>(std::move(node), this);
    }
};

void Node::iterativeCleanClildren()
{
    auto p = this;
    while (!p->children.empty()) {
        while (!p->children.empty()) {
            p = p->back().get(); // go as deep as possible
        }
        if (p != this) {
           p = p->parent; // go back to parent
           p->children.pop_back();
        }
    }
}

这是如何工作的?

  1. 首先它在树(没有子节点的节点)中找到叶子(最右边)
  2. 然后返回父节点并删除刚刚找到的子节点p-&gt;children.pop_back();(这会破坏刚刚找到的叶子的unique_ptr)。
  3. 然后再次找到叶子等等。
  4. 此树清除继续进行,直到到达根 (this) 节点

这种方式根节点完全没有子节点,因为它是迭代实现溢出是不可能的。这棵树有多不平衡并不重要。

【讨论】:

  • 嗯,这段代码应该如何工作?如果节点A 只有一个子节点B,而节点B 只有一个子节点A,您的内部循环将永远循环。如果你禁止创建这样的循环,这段代码就完全是多余的了。
  • 如果您使用智能指针,您可以使用shared_ptr,因为多个节点似乎共享其共同对等点/子节点的所有权。我认为,一旦你使用shared_ptr,就不需要手动析构函数了。
  • 我假设他有一棵树(unique_ptr 暗示它) - 所以图中没有循环。天真的shared_ptr 不是解决方案,因为它会导致强引用循环并以内存泄漏告终。关于这个有nice talk from Herb Sutter。他展示了他的自定义容器,它可以处理循环和垃圾收集它们。他还提到了 list 的 stackoverflow 问题,我只是提供了处理树场景的答案。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2020-11-10
  • 2013-05-25
  • 1970-01-01
  • 1970-01-01
  • 2016-03-13
  • 1970-01-01
  • 2014-03-03
相关资源
最近更新 更多