1

This is my first time working with templates in C++ and I was trying to create a simple Queue data structure that can hold any class that I give it. I made a simple test class just to play around with the implementation and whenever I try to free a node I get a heap corruption error. I suspect it has something to do with the size of test class being much larger than the size of the node but I'm not sure why (or if) that is the case. Any help would be greatly appreciated.

QueueTest.h

#include "../../src/Core/Logging/Logging.h"
#include <iostream>
#include <string>
class TestClass {
    public:
        int num;
        std::string str;
        TestClass(int inNum, std::string inStr);
};


int TestQueueMain(Logger Logs);
int TestEdgeCase1();

QueueTest.cpp

#include "QueueTest.h"
#include "../../src/Core/DataStructures/Queue.h"

TestClass::TestClass(int inNum, std::string inStr) : num(inNum), str(inStr) {}
std::ostream& operator<< (std::ostream &out, TestClass const& data) {
    out << "Num: " << data.num << " Str: " << data.str;
    return out;
}

int TestQueueMain(Logger Logs) {
    Logs.createLog(Info, "Queue tests started", std::source_location::current());
    int failures = 0;
    failures += TestEdgeCase1();

    Logs.createLog(Info, "Queue tests finished with " + std::to_string(failures) + " error(s)", std::source_location::current());
    return failures;
}

int TestEdgeCase1() {
    Queue q = Queue<TestClass>();
    TestClass t1 = TestClass(1, "a");
    std::cout << sizeof(t1) << std::endl; // Prints 48
    q.Enqueue(&t1);
    std::cout << "test" << std::endl;
    TestClass temp = q.Dequeue(); // ERROR HAPPENS HERE
    std::cout << temp;
    
    return 0;
}

Queue.h

#include <iostream>

template <class T>
struct QueueNode {
    T* data; // Takes a class of whatever data it needs to store
    struct QueueNode* next;
};

template <class T>
class Queue {
    public:
        Queue() : Start(nullptr), End(nullptr), Size(0) {}
        ~Queue() { Clean(); }

        template<class T>
        inline void Enqueue(T* Obj) {
            QueueNode<T>* node = (QueueNode<T>*)malloc(sizeof(QueueNode<T>*));
            std::cout << "Inserting obj: " << *Obj << std::endl;
            node->data = Obj;
            node->next = nullptr;
            if (Start == nullptr) {
                Start = node;
                End = node;
            } else {
                End->next = node;
                End = node;
            }
        }

        inline T Dequeue() {
            if (Start == nullptr) return *Start->data; // Returns nullptr
            else if (Start == End) {
                std::cout << "START SIZE: " << sizeof(Start) << std::endl; // prints 8
                std::cout << "START SIZE2: " << sizeof(Start->data) << std::endl; //prints 8
                T val = *Start->data;
                End = nullptr;
                Start->data = nullptr;
                free(Start);
                return val;
            }
            QueueNode<T>* temp = Start;
            T val = *Start->data;
            Start = Start->next;
            free(temp);
            return val;
        }

        inline void Clean() {
            struct QueueNode<T>* temp;
            while (Start != nullptr) {
                temp = Start;
                Start = Start->next;
                free(temp);
            }
            free(Start);
            std::cout << "Cleaning queue" << std::endl;
        }
    private:
        QueueNode<T>* Start;
        QueueNode<T>* End;
        int Size;
};

I have tried freeing the node itself, the data portion of the node and setting it to a nullptr (which Im pretty sure would cause a memory leak) but I keep getting this error.

Bignitse
  • 11
  • 1
  • Is it required for you to write your queue as a linked list? Typically, queues are not written from scratch. You wrap an existing data structure, like a linked list or vector, and force it to behave like a queue via the public interface. If you already have a working linked list, I would suggest just wrapping it instead. – sweenish Mar 21 '23 at 03:29
  • 2
    `if (Start == nullptr) return *Start->data;` huh? – 500 - Internal Server Error Mar 21 '23 at 03:30
  • I would also suggest taking the time to learn to use a debugger. Even a trivial run of the program can get you a stack trace so you can see the calls that lead to your error. – sweenish Mar 21 '23 at 03:31
  • I also see `malloc` and that's a huge red flag for C++. Inconsistent use of `template `. You were allowed to be inconsistent because it's not necessary with how you've written the code. The node is an implementation detail and should not be publicly visible, let alone global. – sweenish Mar 21 '23 at 03:38
  • 2
    The code is broken right from the start. `QueueNode* node = (QueueNode*)malloc(sizeof(QueueNode*));` -- This is a non-starter. You did not create a `QueueNode` object -- all you did was allocate a bunch of bytes that are not anything but a bunch of bytes. Where did you get the idea to use `malloc` in a C++ program? Certainly not a good C++ book, as every good C++ book does not show usage of `malloc` this way. If anything, `new[]` and `new` are shown and used. Then everything else, for the most part, has to be rewritten and reimplemented. – PaulMcKenzie Mar 21 '23 at 04:24
  • 2
    @PaulMcKenzie and, it's not even allocating enough bytes at that. It is allocating the size of a pointer, not the size of a queue object. – Remy Lebeau Mar 21 '23 at 04:33
  • 1
    @RemyLebeau -- Yes, I noticed that also. The code basically has to be rewritten from scratch, with the knowledge of how to properly create objects, the rule-of-3, etc. It's mostly "I know C++ syntax, so here is the attempt", but knowledge of C++ syntax does not put together an actual working, coherent C++ program. – PaulMcKenzie Mar 21 '23 at 04:39
  • `inline void Enqueue(T* Obj)` -- The user shouldn't have to pass pointers to `Enqueue`. If the Queue is supposed to hold integers, i.e. `Queue Q; Q.Enqueue(1); Q.Enqueue(2);`, etc. this should work correctly. It is the responsiblity of the `Queue` class to figure out how to place those integers into the queue by allocating the space for those entities. Ideally, the prototype should be: `inline void Enqueue(const T& Obj)` – PaulMcKenzie Mar 21 '23 at 05:04
  • When a template misbehaves, re-write your test case as a non-template (as in write a class named `QueueTestClass` whose definition is what you expect `Queue` to be). Test with the non-template to see if "template" is really part of the problem, or if it is a red herring. – JaMiT Mar 21 '23 at 07:51
  • @PaulMcKenzie Appreciate the response. My knowledge is almost entirely in C and it has been some time since I've worked in it. Also the reason I am using Malloc over New is because I read that Malloc is more efficient than new due to new allocating memory and also calling the constructor by default. Also the queue is mainly being created to hold Event objects which hold data on different events that are occurring in a game engine I am writing (trying to learn more about what goes on under the hood in game engines). – Bignitse Mar 21 '23 at 14:25
  • @sweenish I have been using the visual studio debugger and that is how I figured out where the error was actually occurring. What it doesn't tell me is why it is occurring. I noticed a couple of comments that malloc is not good in c++. Are there any good resources I can read to further clarify this? I thought the whole point of C++ was being able to manage memory the same way you can in C but with Object Oriented principals. – Bignitse Mar 21 '23 at 14:29
  • @Bignitse You can't use `malloc()` to create non-POD types as what you're doing now. `C` has no concept of non-POD types, constructors, destructors, virtual functions, etc. Creating classes that has these things using `malloc` will not work. You must properly construct your objects in C++ before you can use them -- `malloc`-ing `N` bytes, where `N` is the size of the object is *not* how it's done in C++. For example, `std::string *p = (std::string*)malloc(sizeof(std::string));` does **not** give you a pointer to a valid `std::string` object. – PaulMcKenzie Mar 21 '23 at 14:31
  • @PaulMcKenzie Ah okay interesting. I thought that it would work a lot closer to the way plain C works but I guess I was wrong. Thank you for the explanation, I will scrap this after work and rewrite it. – Bignitse Mar 21 '23 at 14:38
  • @Bignitse [See this piece of code](https://godbolt.org/z/Eo4P4e7Ev). It indicates the differences between a trivially-copyable type, and one that isn't. The non-triviallycopyable type has a single `std::string` member. Just that alone stops `malloc` from working to give you a valid object. BTW, *malloc* can be used, but in the context of using it for [placement-new](https://stackoverflow.com/questions/222557/what-uses-are-there-for-placement-new), which your code does not do. – PaulMcKenzie Mar 21 '23 at 14:44
  • @Bignitse *"the reason I am using Malloc over New is because I read that Malloc is more efficient than new due to new allocating memory and also calling the constructor by default."* Interestingly, the reason you should prefer `new` over `malloc` is the same reason (new allocates memory and also calls the constructor). – JaMiT Mar 22 '23 at 00:35

0 Answers0