Ziv Riger
Ziv Riger

Reputation: 39

C++ destructor being called unexpectedly doubly linked list

    doubly_linked_list::~doubly_linked_list()
{
    list_item* current = head;
    while (current)
    {
        list_item* next = current->Get_Next();
        delete current;
        current = next;
    }
}

I am writing a dynamic graph in C++ with the usage of a doubly linked list to save the Graph nodes, however my code keeps on failing as it calls the destructor more times than it should be calling it, for example, during my destructor for the graph

    Dynamic_Graph::~Dynamic_Graph()
{
    edges.~doubly_linked_list();
    nodes.~doubly_linked_list();
}

the destructor for the doubly linked list is called more than 2 times, despite it being called explicitly twice.

then i have the function for insertion of an edge:

Graph_Edge* Dynamic_Graph::Insert_Edge(Graph_Node* from, Graph_Node* to)
{
    Graph_Edge* new_edge = new Graph_Edge(from, to);
    from->Get_out_Nodes().List_Insert_After(to, from->Get_out_Nodes().Get_Tail());
    to->Get_in_Nodes().List_Insert_After(from, to->Get_in_Nodes().Get_Tail());
    edges.List_Insert(new_edge);
    return new_edge;
}

after adding the to node to the adjacency list of the from node the code unexplainably calls the destructor for a doubly linked list and deletes this insertion, however i am not sure why.

here are the headers for the graph and the linked list:

class doubly_linked_list
{
private:
    list_item* head;
    list_item* tail;
public:
    doubly_linked_list() { this->head = NULL; this->tail = NULL; }
    doubly_linked_list(list_item* head){ this->head = head; this->tail = NULL; }
    ~doubly_linked_list();
    list_item* Get_Head() { return head; }
    list_item* Get_Tail() { return tail; }
    void Set_Head(list_item* h) { head = h; }
    void List_Insert(list_item* x);
    void List_Insert_After(list_item* x, list_item* y);
    void List_Delete(list_item* x);
};


class Dynamic_Graph
{
protected:
    doubly_linked_list edges;
    doubly_linked_list nodes;
public:
    Dynamic_Graph() { edges = doubly_linked_list(); nodes = doubly_linked_list(); }
    ~Dynamic_Graph();
    Graph_Node* Insert_Node(unsigned node_key);
    void Delete_Node(Graph_Node* node);
    Graph_Edge* Insert_Edge(Graph_Node* from, Graph_Node* to);
    void Delete_Edge(Graph_Edge* edge);
    Rooted_Tree* SCC() const;
    Rooted_Tree* BFS(Graph_Node* source) const;
};

any help would be welcome

EDIT: i removed the calls to the destructors, however i still am getting a destructor call in the insert after function and i am not sure why.

EDIT 2: adding more relevant lines of code:

Graph_Edge* Dynamic_Graph::Insert_Edge(Graph_Node* from, Graph_Node* to)
{
    Graph_Edge* new_edge = new Graph_Edge(from, to);
    from->Get_out_Nodes().List_Insert_After(to, from->Get_out_Nodes().Get_Tail());
    to->Get_in_Nodes().List_Insert_After(from, to->Get_in_Nodes().Get_Tail());
    edges.List_Insert(new_edge);
    return new_edge;
}

this is the function that triggers the error where it access a deleted pointer despite it not supposed to be deleted, it triggers after finishing inserting the "to" node to the "from" node adjacency list.

void doubly_linked_list::List_Insert_After(list_item* x, list_item* y)
{
    if (!y)
    {
        head = x;
        tail = x;
        return;
    }
    if (y == tail)
    {
        tail = x;
    }
    if (y->Get_Next())
    {
        y->Get_Next()->Set_Prev(x);
    }
    x->Set_Next(y->Get_Next());
    x->Set_Prev(y);
    y->Set_Next(x);
}

this function is the insert after function in the doubly linked list.

EDIT 3: i tried to recrating the issue via the smallest amount of possible function, this is what i got:

#pragma once
#include < cstddef >
class List_Item
{
protected:
    List_Item* next;
    List_Item* prev;
public:
    List_Item(): next(NULL), prev(NULL) {}
    List_Item* Get_Next() { return next; }
    List_Item* Get_Prev() { return prev; }
    void Set_Next(List_Item* next) { this->next = next; }
    void Set_Prev(List_Item* prev) { this->prev = prev; }
    List_Item(const List_Item& item) : next(item.next), prev(item.prev){}
    List_Item& operator=(const List_Item& item) { this->next = item.next; this->prev = item.prev; return *this; }
};

class Doubly_Linked_List
{
protected:
    List_Item* head;
    List_Item* tail;
public:
    Doubly_Linked_List() : head(NULL), tail(NULL) {}
    ~Doubly_Linked_List();
    List_Item* Get_Head() { return head; }
    List_Item* Get_Tail() { return tail; }
    void Set_Head(List_Item* h) { head = h; }
    void Set_Tail(List_Item* t) { tail = t; }
    void insert(List_Item* item);
    void insert_after(List_Item* item, List_Item* after);
    Doubly_Linked_List(const Doubly_Linked_List& list) : head(list.head), tail(list.tail) {}
    Doubly_Linked_List& operator=(const Doubly_Linked_List& list) { this->head = list.head; this->tail = list.tail; return *this; }
};
Doubly_Linked_List::~Doubly_Linked_List()
{
    List_Item* item = head;
    while (item)
    {
        List_Item* next = item->Get_Next();
        delete item;
        item = next;
    }
}

void Doubly_Linked_List::insert(List_Item* item)
{
    item->Set_Next(head);
    if (head)
        head->Set_Prev(item);
    if (!head)
        tail = item;
    head = item;
    item->Set_Prev(NULL);
}

void Doubly_Linked_List::insert_after(List_Item* item, List_Item* after)
{
    if (!after)
    {
        head = item;
        tail = item;
        return;
    }
    if (after->Get_Next())
    {
        after->Get_Next()->Set_Prev(item);
    }
    else
    {
        tail = item;
    }
    item->Set_Next(after);
    item->Set_Prev(after);
    after->Set_Next(item);
}


#pragma once
#include "List_Item.h"
#include"Doubly_Linked_List.h"
class Graph_Node :
    public List_Item
{
protected:
    const unsigned key;
    Doubly_Linked_List in_nodes;
    Doubly_Linked_List out_nodes;
public:
    Graph_Node(unsigned key): key(key) {}
    Doubly_Linked_List Get_In_Nodes() { return in_nodes; }
    Doubly_Linked_List Get_Out_Nodes() { return out_nodes; }
};



   class Graph_Edge :
    public List_Item
{
protected:
    Graph_Node* from;
    Graph_Node* to;
public:
    Graph_Edge(Graph_Node* f, Graph_Node* t): from(f), to(t) {}
    Graph_Node* Get_From() { return from; }
    Graph_Node* Get_To() { return to; }
};

int main()
{

    unsigned node_key = 1;
    Graph_Node* from = new Graph_Node(node_key++);
    Graph_Node* to = new Graph_Node(node_key++);
    from->Get_Out_Nodes().insert_after(to, from->Get_Out_Nodes().Get_Tail());
    to->Get_In_Nodes().insert_after(from, to->Get_In_Nodes().Get_Tail());
}

Upvotes: 0

Views: 235

Answers (1)

walnut
walnut

Reputation: 22152

You are not supposed to call destructors manually. They are called automatically for you.

You only need to use delete (which also calls the destructor) and only on objects that you have actually created with a call to new. Every other object will be destroyed automatically when the end of its scope is reached and its destructor will then be called automatically. This is also true for members of classes.

The only exception here is if you used a placement-new to create the object or if you intend to reuse its storage. But you probably don't intend to do something like this.

Remove the destructor of Dynamic_Graph completely. It is not needed.


Your class doubly_linked_list violates the rule of 0/3/5. The rule says that if you define a custom destructor, then you should also define a custom copy(/move) constructor and copy(/move) assignment operator.

If you violate this rule and you happen to make an implicit or explicit copy of an object of that class, then you are likely going to have undefined behavior because the destructors will be called twice and try to delete memory twice.


After question edit with a full example:

In the code you are showing now the destructor for Doubly_Linked_List is called four times (see https://godbolt.org/z/KmStwy). These calls are because Get_In_Nodes and Get_Out_Nodes return Doubly_Linked_List lists by-value as copies of the class members of Graph_Node. Since you call these functions four times in main, there are four Doubly_Linked_List temporary objects that need to be destructed (which the compiler does automatically).

You did not properly implement Doubly_Linked_List(const Doubly_Linked_List& list) (and the copy assignment operator) to actually copy the whole list, so your program still has undefined behavior. You need to implement the copy operations in such a way that the whole list is being deep copied, otherwise the destructor call of both the originial and the copy will delete the same nodes.

Alternatively you can define the copy operations as deleted:

Doubly_Linked_List(const Doubly_Linked_List& list) : head(list.head), tail(list.tail) = delete
Doubly_Linked_List& operator=(const Doubly_Linked_List& list) = delete;

in which case the compiler will not allow copying and give a compile-time error if you try to copy. But your current implementation is just as broken as it was before. You basically just copied what the implicit copy operations do. Read the link about the rule of 0/3/5 again and carefully since it is extremely important to avoid undefined behavior.

The fact that the destructor is called multiple times is not by itself a problem. That is normal if copies are involved. The problem is only that your class copy operations are broken. If you don't want to create copies in the Get_In_Nodes and Get_Out_Nodes though, then return by-reference instead.


In general you should not use new/delete in the way that you are doing in modern C++. Instead you can use std::make_unique and std::unique_ptr for owning pointers and if you do so, then none of your shown classes will need custom destructors (at least not custom destructors that have effective behavior different from the implicit one) and you can always use the rule of 0, meaning that you don't need to implement any of the copy operations either.

And if this is anything but a learning exercise, you shouldn't write your own list in the first place. std::list works fine and is almost surely superior to what you are coming up with.

Upvotes: 1

Related Questions