【问题标题】:Circular data dependency destructor循环数据依赖析构函数
【发布时间】:2016-09-02 11:13:53
【问题描述】:

我现在正在设计我自己的带有邻接列表的图形类。我完成了除析构函数之外的大部分步骤。

这是我的 Vertex 类:

struct Vertex{
public:
    Vertex(){m_name="";}
    Vertex(string name):m_name(name){}
    ~Vertex(){
        cout << "vertex des" << endl;
        for(int i = 0; i < m_edge.size(); i++){
            delete m_edge[i];
            m_edge[i] = nullptr;
        }
    }
    string m_name;
    vector<Edge*> m_edge;
};

这是我的 Edge 课程:

struct Edge{
public:
    Edge() : m_head(nullptr), m_tail(nullptr) {m_name="";}
    Edge(string name) : m_name(name), m_head(nullptr), m_tail(nullptr) {}

    ~Edge(){
        cout << "Edge des" << endl;

        delete m_head;
        m_head = nullptr;
        delete m_tail;
        m_tail = nullptr;
    }

   string m_name;

   Vertex* m_head;
   Vertex* m_tail;
};

但是,我注意到当调用析构函数时,两个类实际上都调用了彼此的析构函数,所以这给了我一个无限循环。这种设计有问题吗?如果没有,有没有办法解决这个析构函数问题?谢谢!

【问题讨论】:

  • 哪一个是“所有者”,哪一个是“拥有”?所有者进行删除。
  • 对应deletes的new()在哪里?

标签: c++ oop c++11 graph


【解决方案1】:

但是,我注意到当调用析构函数时,两个类实际上都调用了彼此的析构函数,所以这给了我一个无限循环。这种设计有问题吗?

您当前的设计确实有问题。动态分配只能由其各自的所有者删除。通常,所有者是创建对象的人,并且通常只有一个所有者。如果有多个对象,则所有权共享。共享所有权需要一种机制(例如引用计数)来跟踪当前所有者的数量。

根据析构函数判断,您的顶点似乎由多个边“拥有”,而这些边似乎由多个顶点拥有。如果不是这种情况,那么您的图表将非常无聊。但您尚未实施任何形式的所有权跟踪。

我建议使用更简单的设计,其中边不拥有顶点,顶点也不拥有边。它们都应该由一个可能称为Graph 的父对象拥有。

【讨论】:

  • 是的,graph 拥有顶点和边,边只是对它们两个顶点的引用。如果没有令人信服的理由使用动态分配(例如顶点类的动态继承),那么最简单的方法就是删除它。如果确实有必要,并且由于问题标记为 C++11,则应使用智能指针明确所有权。
  • 我同意@dureuill:一个将 shared_ptr's 保存到边和顶点并且它们彼此保存weak_ptr's 的图至少使破坏自动和可检查(没有悬空指针,可检查,因为你仍然应该检查使用前锁成功)
  • @user2079303:不,它没有,因为边和顶点持有weak_ptr。该图表按顺序发布它们。我认为您对另一个答案感到困惑。
  • @stefaanv 哦,对不起,我没有足够注意你的描述。实际上,在顶点和边中使用弱指针没有我提到的这样的问题。尽管如此(如果需要多态节点)我不排除在图形中使用unique_ptrs 和在节点/边中使用普通指针,当性能受到关注时,这通常是 - 尽管并非总是 - 图形的情况。我完全同意 dureuill。
  • 谢谢!会改变我的结构!
【解决方案2】:

由于问题标记为 C++11,因此您应该首先使用托管指针。在托管指针中,weak_ptr可以帮你打破循环依赖:

#include <vector>
#include <memory>
#include <string>
#include <iostream>

using namespace std;

struct Edge;

struct Vertex{
public:
    Vertex(){m_name="";}
    Vertex(string name):m_name(name){}
    ~Vertex(){
        cout << "vertex des" << endl;
        for(auto e : m_edge)
        {
            if(e->m_head.lock().get() == this)
            {
                e->m_head = nullptr;
            }
            if(e->m_tail.lock().get() == this)
            {
                e->m_tail = nullptr;
            }
        }
    string m_name;
    vector<shared_ptr<Edge>> m_edge;
};

这里你的原始指针被更改为shared_ptrs:不需要在销毁时调用 delete,但你应该告诉边缘忘记顶点(参见下面的头和尾声明)。

struct Edge{
public:
    Edge(){m_name="";}
    Edge(string name):m_name(name){}

    ~Edge(){
        cout << "Edge des" << endl;
        // if you're here, this means no vertices points to the edge any more:
        // no need to inform head or tail the edge is destroyed
    }

    void do_something_to_head()
    {
        auto head = m_head.lock();
        if(head)
        {
            head->do_something();
        }
    }

   string m_name;

   weak_ptr<Vertex> m_head;
   weak_ptr<Vertex> m_tail;
};

边缘中的原始指针已更改为weak_ptr:它们是指向共享资源的“非拥有”对象。你不能直接取消引用weak_ptr:你必须调用方法lock,如果它存在的话,它会为指向的资源创建一个临时的shared_ptr(从而防止循环依赖)。用法:

int main()
{
    shared_ptr<Vertex> v1 = make_shared<Vertex>();
    shared_ptr<Vertex> v2 = make_shared<Vertex>();

    // connection should be encapsulated somewhere

    shared_ptr<Edge> e = make_shared<Edge>();

    v1->m_edge.push_back(e);
    e->m_head = v1;

    v2->m_edge.push_back(e);
    e->m_tail = v2;

    return 0;
}

【讨论】:

    【解决方案3】:

    我认为,这是一个设计问题,因为在图形方面 - 当你删除边缘时 - 你不应该删除它的顶点。

    我想,

    m_head = nullptr;
    m_tail = nullptr;
    

    在你的例子中就足够了。

    【讨论】:

    • 那么(在析构函数中)就完全没有必要了吗?
    • 看看它的构造函数:(我认为那里跳过了部分代码)但是 Edge 并没有构造它的顶点,它只是引用它们。所以我的解决方案只是取消引用(为了清楚起见)。在其他类中,它保存指针而不是删除它们 - 这将是内存泄漏
    • 我不明白你想告诉我什么?我认为没有必要在析构函数中将这些指针设置为nullptr。当然,它们不应该在那里被删除(我推测是这样的)。
    • 是的,我没有从第一条评论中得到你。我以为你在谈论删除。而且 - 是的 - 这不是必需的,因为指针已经消失了。尽管如此,我还是会在析构函数中留下类似这样的代码,只是为了清楚起见 m_head 和 m_tail 不再有这个优势。未来可能会更改设计,因此这将是开销很小的自我注释代码
    • @grindaah 从对象即将完全消失的事实中可以清楚地看出这一点。这有点像在你将要扔进火山的书的每一页上坚持写“请忽略”。
    【解决方案4】:

    就像其他人已经回答的那样,对您的问题的简短回答是:是的,调用 eachover 的析构函数是有问题的,因为这可能会导致 未定义的行为

    例如,看这种情况:

    • 正在删除 Vertex 对象 v
    • 这会导致删除成员向量m_edge 中的第一个Edge
    • 这会导致m_headm_tail Vertex 的删除,
    • 假设其中之一是v,那么v 的析构函数中的下一个循环将尝试访问已删除的数据!!!

    充其量,您的程序会出现段错误;最坏的情况……谁知道呢……

    你的设计还不错。然而,它的问题是您无法明确定义所有权(这有助于了解谁应该摧毁谁)。

    确实,假设Vertex 可以与多个(至少一个)Edge 相关,并且Edge 与恰好两个Vertex 相关,那么您可以认为Edge由一对Vertex 拥有。在那种情况下管理删除顺序并不容易......

    但是,您并不绝对需要与谁应该摧毁谁的国家建立所有权关系。如上所述,一个Edge 恰好与两个Vertex 相关;如果其中一个被破坏,那么Edge 也应该被破坏。另一方面,如果Edge 被销毁,则没有理由销毁与其相关的任何Vertex,因为每个Edge 仍然可以与其他现有Edge 相关;唯一的例外是Vertex 与任何Edge 不再有任何关系。遵循这些规则的代码如下:

    struct Edge;
    
    struct Vertex {
    public:
        // ctors unchanged
        ~Vertex();  // implemented below
        void remove_relation(Edge* edge)    // for use by Edge only
        {
            std::vector<Edge*>::iterator it =
                std::find(m_edge.begin(), m_edge.end(), edge);
            if (it != m_edge.end())
                 m_edge.erase(it);
            if (m_edge.size() == 0)
                delete this;  // this Vertex can be safely deleted
        }
        string        m_name;
        vector<Edge*> m_edge;
    };
    
    struct Edge {
    public:
        // ctors unchanged
        ~Edge() {
            // Only have to remove relation with m_head & m_tail
            if (m_head)
                m_head->remove_relation(this);
            if (m_tail)
                m_tail->remove_relation(this);
            std::cout << "deleted Edge " << this << std::endl;
        }
        void delete_from_vertex(Vertex* from)  // for use by Vertex only
        {
            // Prevent from removing relation with the calling Vertex
            if (m_head == from)
                m_head = nullptr;
            else if (m_tail == from)
                m_tail = nullptr;
            else
                assert(false); // Vertex not in relation with this Edge
            delete this;       // finally destroy this Edge
        }
       string  m_name;
       Vertex* m_head;
       Vertex* m_tail;
    };
    
    Vertex::~Vertex() {
        for(int i = 0; i < m_edge.size(); i++)
            m_edge[i]->delete_from_vertex(this);    // special destruction
        std::cout << "deleted Vertex " << this << std::endl;
    }
    

    Live example

    【讨论】:

      猜你喜欢
      • 2014-08-03
      • 1970-01-01
      • 2011-04-08
      • 1970-01-01
      • 2015-08-30
      • 1970-01-01
      • 2017-10-19
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多