Matteo Bonacini
Matteo Bonacini

Reputation: 55

C++ class destructor not being called automatically causes memory leak?

(Before anyone asks: no, i didn't forget the delete[] statement)

I was fiddling around with dinamically allocated memory and i run into this issue. I think the best way to explain it is to show you these two pieces of code I wrote. They are very similar, but in one of them my class destructor doesn't get called.

// memleak.cpp
#include <vector>

using namespace std;

class Leak {
    vector<int*> list;

  public:
    void init() {
        for (int i = 0; i < 10; i++) {
            list.push_back(new int[2] {i, i});
        }
    }

    Leak() = default;
    ~Leak() {
        for (auto &i : list) {
            delete[] i;     
        }
    }
};

int main() {
    Leak leak;
    while (true) {
        // I tried explicitly calling the destructor as well,
        // but this somehow causes the same memory to be deleted twice
        // and segfaults
        // leak.~Leak();
        leak = Leak();
        leak.init();
    }
}

// noleak.cpp
#include <vector>

using namespace std;

class Leak {
    vector<int*> list;

  public:
    Leak() {
        for (int i = 0; i < 10; i++) {
            list.push_back(new int[2] {i, i});
        }
    };
    ~Leak() {
        for (auto &i : list) {
            delete[] i;     
        }
    }
};

int main() {
    Leak leak;
    while (true) {
        leak = Leak();
    } 
}

I compiled them both with g++ filename.cpp --std=c++14 && ./a.out and used top to check memory usage.

As you can see, the only difference is that, in memleak.cpp, the class constructor doesn't do anything and there is an init() function that does its job. However, if you try this out, you will see that this somehow interferes with the destructor being called and causes a memory leak.

Am i missing something obvious? Thanks in advance.

Also, before anyone suggests not using dinamically allocated memory: I knwow that it isn't always a good practice and so on, but the main thing I'm interested in right now is understanding why my code doesn't work as expected.

Upvotes: 1

Views: 629

Answers (2)

Quentin
Quentin

Reputation: 63154

Your class doesn't respect the rule of 3/5/0. The default-generated move-assignment copy-assignment operator in leak = Leak(); makes leak reference the contents of the temporary Leak object, which it deletes promptly at the end of its lifetime, leaving leak with dangling pointers which it will later try to delete again.

Note: this could have gone unnoticed if your implementation of std::vector systematically emptied the original vector upon moving, but that is not guaranteed.

Note 2: the striked out parts above I wrote without realizing that, as StoryTeller pointed out to me, your class does not generate a move-assignment operator because it has a user-declared destructor. A copy-assignment operator is generated and used instead.

Use smart pointers and containers to model your classes (std::vector<std::array<int, 2>>, std::vector<std::vector<int>> or std::vector<std::unique_ptr<int[]>>). Do not use new, delete and raw owning pointers. In the exceedingly rare case where you may need them, be sure to encapsulate them tightly and to carefully apply the aforementioned rule of 3/5/0 (including exception handling).

Upvotes: 1

This is constructing a temporary object and then assigning it. Since you didn't write an assignment operator, you get a default one. The default one just copies the vector list.

In the first code you have:

  1. Create a temporary Leak object. It has no pointers in its vector.
  2. Assign the temporary object to the leak object. This copies the vector (overwriting the old one)
  3. Delete the temporary object, this deletes 0 pointers since its vector is empty.
  4. Allocate a bunch of memory and store the pointers in the vector.
  5. Repeat.

In the second code you have:

  1. Create a temporary Leak object. Allocate some memory and store the pointers in its vector.
  2. Assign the temporary object to the leak object. This copies the vector (overwriting the old one)
  3. Delete the temporary object, this deletes the 10 pointers in the temporary object's vector.
  4. Repeat.

Note that after leak = Leak(); the same pointers that were in the temporary object's vector are also in leak's vector. Even if they were deleted.

To fix this, you should write an operator = for your class. The rule of 3 is a way to remember that if you have a destructor, you usually need to also write a copy constructor and copy assignment operator. (Since C++11 you can optionally also write a move constructor and move assignment operator, making it the rule of 5)

Your assignment operator would delete the pointers in the vector, clear the vector, allocate new memory to hold the int values from the object being assigned, put those pointers in the vector, and copy the int values. So that the old pointers are cleaned up, and the object being assigned to becomes a copy of the object being assigned from, without sharing the same pointers.

Upvotes: 2

Related Questions