0

i've been trying to figure it out by my self but I cant figure it out, I've try to implement a doubly circular linked list template class, in purpose to create a menu for an app with SFML.

I dont know why but when i implement an instance of CDblLinkedList (int,string,sf::Text) it work without any error, but when i try to insert in CDblLinkedList an instance of CSubMenu i got an error Exception has occurred. EXC_BAD_ACCESS (code=1, address=0x170). When i debug it i've saw that at the second call of the destructor ~CDblLinkedList(), head->GetPrev()->SetNext(nullptr); generate this error because the prev pointer of the head is equal to nullptr.

CNode.h

#pragma once
template <typename T>
class CNode{

    private:
        CNode<T>*prev=nullptr;
        T data;
        CNode<T>*next=nullptr;

    public:
        CNode(T p_data);
        void SetNext(CNode<T>*p_next){next=p_next;};
        void SetPrev(CNode<T>*p_prev){prev=p_prev;};
        CNode<T>* GetNext(void){return(next);};
        CNode<T>* GetPrev(void){return(prev);};
        T* GetData(void){return(&data);};

};

template <typename T>
CNode<T>::CNode(T p_data):data(p_data){
   // data=p_data;
}

CDblLinkedList.h

#pragma once
#include "CNode.h"
#include <iostream>
template <typename T>
class CDblLinkedList{
    private:
        CNode<T>*head=nullptr,*current=nullptr;
    public:
        CDblLinkedList();
        void Insert(T p_data);
        void Next();
        void Prev();
        CNode<T>*GetCurr(void){return(current);};
        CNode<T>*GetHead(void){return(head);};
        ~CDblLinkedList();
};

template<typename T>
CDblLinkedList<T>::CDblLinkedList(){
}

template<typename T>
void CDblLinkedList<T>::Insert(T p_data){
    CNode<T> * dummy=new CNode<T>(p_data);
    if(!head){
        head=dummy;
        head->SetNext(head);
        head->SetPrev(head);
        current=head;
    }else{
        dummy->SetPrev(head->GetPrev());
        dummy->SetNext(head);
        head->GetPrev()->SetNext(dummy);
        head->SetPrev(dummy);
    }
}
template<typename T>
void CDblLinkedList<T>::Next(){
    current=current->GetNext();
}
template<typename T>
void CDblLinkedList<T>::Prev(){
    current=current->GetPrev();
}

template<typename T>
CDblLinkedList<T>::~CDblLinkedList(){
    if(head){
       head->GetPrev()->SetNext(nullptr);
        CNode<T>* dummy=nullptr;
        while(head){
            dummy=head;
            head=head->GetNext();
            delete dummy;
            dummy=nullptr;
        }
        head=nullptr;
        current=nullptr;
    }
}

main.cpp

#include<SFML/Graphics.hpp>
#include "CSubMenu.h"
int main(){
    sf::RenderWindow window({ 1600 ,900 }, "test");
    sf::Font font;
    font.loadFromFile("./assets/fonts/Oswald-Bold.ttf");
    CSubMenu left(300,450,"AlgoText",font,&window);
    left.AddChoice("1- BFS");
    left.AddChoice("2- DFS");
    left.AddChoice("3- A*");
    CDblLinkedList<CSubMenu>list;
    list.Insert(left);

    return 0;
}

I've try to create separate CSubMenu instance it work, but not when i try to insert one of them in a CDblLinkedList instance.

Thank you in advance :)

RayanYI
  • 1
  • 1
  • 1
    Before I even look at the code, have you tried debugging with valgrind? It helps a lot with finding memory related errors. – Refugnic Eternium Mar 14 '23 at 12:05
  • 1
    Another general advice: Step through your code. Keep an eye on those variables. When does the behaviour differ from what you're expecting? Once you've found that line, you're almost there. – Refugnic Eternium Mar 14 '23 at 12:07
  • Um...do I see that right? You are assigning the head as both 'the previous' and 'the next' of itself? How will you ever know you're dong iterating? – Refugnic Eternium Mar 14 '23 at 12:09
  • `dummy->SetPrev(head->GetPrev());` This line looks suspicious in Insert when head is nullptr. – kiner_shah Mar 14 '23 at 12:10
  • While you're stepping through the code in a [*debugger*](https://stackoverflow.com/questions/25385173/what-is-a-debugger-and-how-can-it-help-me-diagnose-problems), also draw the list and all the operations you do on it on a piece of paper. Use squares for the nodes, use arrows for the pointers/links. Erase and redraw arrows and you modify them in the code. Does the drawing make sense when the crash happens? – Some programmer dude Mar 14 '23 at 12:11
  • 2
    Why don't you use `std::list` which is a doubly linked list and instead implement your own solution? – kiner_shah Mar 14 '23 at 12:24
  • The *second* call of the destructor? There shouldn't be a second one. What's `CSubMenu`? – molbdnilo Mar 14 '23 at 12:35
  • @OP If your goal is to do something with graphics, you shouldn't waste time in achieving this goal by trying to write your own linked list class -- use the tools that C++ has to offer. If you value your time, as mentioned previously, use `std::list`. – PaulMcKenzie Mar 14 '23 at 12:53
  • 1
    @kiner_shah because I need a circular doubly linked list, but std::list is not circular – RayanYI Mar 14 '23 at 13:15
  • @RefugnicEternium thanks i will try to use valgrind and yes I’m assigning the head as both 'the previous' and 'the next' of itself for the circular. to know when i’m donne in the destructor i set the link between the head and last node to nullptr – RayanYI Mar 14 '23 at 14:03
  • 1
    @RayanYI -- Use [Boost circular buffer](https://www.boost.org/doc/libs/1_81_0/doc/html/circular_buffer.html). That is, if you value your time. – PaulMcKenzie Mar 14 '23 at 16:34
  • @PaulMcKenzie Thanks you, if I can't solve the problem, I will use it :) – RayanYI Mar 14 '23 at 17:54
  • @RayanYI -- The good thing is that the circular list is a header-only library, so no need to link anything -- just include the boost header (of course, you have to download boost). – PaulMcKenzie Mar 14 '23 at 18:02

1 Answers1

0

Implicit copies

You see multiple destructor calls, because copies of your type T object are created. See your modified code example that shows the constructor/destructor calls. Inserting a single element to list leads to multiple destructor calls, because multiple copies of T are made in the process.

This could explain why using type CDblLinkedList<int> works, but not CDblLinkedList<CSubMenu>. The latter most likely has no proper copy constructor.

There are plenty of ressources that explain this issue. In short: If you copy an object that holds a pointer and then at the end of its lifetime the destructor calls a delete on that pointer, you must write a copy constructor or you will end up with double delete issues. Because both - the original and the copied object - point to same address and therefore try to delete the same thing when destroyed.

Copy locations

First copy occurs when copying the argument in CNode.h:

list.Insert(left); 

Second copy: p_data is copied when calling the constructor of CNode while in function CDblLinkedList<T>::Insert:

CNode<T>* dummy = new CNode<T>(p_data); 

Third copy: Copy p_data argument to p_data member in CNode<T> constructor:

CNode<T>::CNode(T p_data) : data(p_data) {
    // data=p_data;
}

Solution

A) Provide a bug-free copy constructor for T similar to your modified code example.

B) Avoid copies of T that cause problems by providing a move constructor and then modify the code so that T is moved instead copied.

C) Or decouple lifetime issues from your class CDblLinkedList by only passing it a reference or a pointer to your Item, eg. use CDblLinkedList<CSubMenu&>, as demonstrated in this example on compiler-explorer. Additionally, to prevent unintended copies, explicitly delete the copy constructor: Foo(const Foo& other) = delete;

leosch
  • 451
  • 2
  • 10
  • Thanks you for your explanation, im still having some issues with it i'm still trying to debug it :) – RayanYI Mar 14 '23 at 17:52
  • The answer should be clearer after my edit. If you are stuck, I suggest you post the code for `CSubMenu`. It can be a reduced version though, as long as the problem can be reproduced. – leosch Mar 15 '23 at 14:00
  • 1
    Yes, it's clearer thanks. I do not know about the rule of 5 in c++, my CSubMenu has a linked list of sf::Text for attribute, i think it should be about the copy, that's why there is a double free memory at the same adress. – RayanYI Mar 16 '23 at 11:38
  • Rule of 0/3/5 is the guideline for RAII. If your class initialization envolves resource aquisition, then the destructor must release it. If any of the 3 special members(copy constructor, assignment operator, destructor) are defined, then the other 2 must be properly defined too; otherwise resource management bugs appear. Rule of 5 regards optimizations involving move construction and move assignment. You have to provide either 0 or 3 or 5 of the special member functions to aviod resource bugs. – Red.Wave Mar 16 '23 at 12:21
  • @Red.Wave Ok thanks, so even if we use c++11 it's not mandatory to use the rule of 5 we can just use rule of 3 im i right ? – RayanYI Mar 16 '23 at 15:53
  • @RayanYI, nothing is mandatory. These are just guidelines. Rule of 3 is as old as C++. Rule of 5 is the result of applying move semantics from C++11 on. If the class declared a none-trivial destructor, it is most probably move-only (needs move ctor and move assign), or copyable(needs at least copy ctor and copy assign), or none-movable(disable all 4 special functions by `=delete`ing the move ctor). There is a famous chart of special member functions for Rule of 0/3/5. Just google it. – Red.Wave Mar 16 '23 at 17:15
  • https://www.foonathan.net/images/special-member-functions.png – Red.Wave Mar 16 '23 at 17:32
  • @Red.Wave Ok thanks you i will study it :) – RayanYI Mar 16 '23 at 19:52
  • @RayanYI ***RAII** is a must for C++progrmmers*. **Rule of 0/3/5** is a crucial idiom to correctly implement RAII. There's an obsolete **copy/swap idiom**, which is good for simple realization *of rule of 3*; it is the 1'st solution, but - as you improve - you'll see there are better alternatives for specific classes. – Red.Wave Mar 16 '23 at 20:10