1

I'm trying to store structs in a vector. Struct needs to dynamically allocate memory for char* of a given size. But as soon as I add the struct to a vector, its destructor gets called, as if I lost the pointer to it.

I've made this little demo for the sake of example.

#include "stdafx.h"
#include <iostream>
#include <vector>

struct Classroom
{
    char* chairs;

    Classroom() {} // default constructor

    Classroom(size_t size)
    {
        std::cout << "Creating " << size << " chairs in a classroom" << std::endl;
        chairs = new char[size];
    }

    ~Classroom()
    {
        std::cout << "Destroyng chairs in a classroom" << std::endl;
        delete[] chairs;
    }
};

std::vector<Classroom> m_classrooms;

int main()
{

    m_classrooms.push_back(Classroom(29));
    //m_classrooms.push_back(Classroom(30));
    //m_classrooms.push_back(Classroom(30));

    system("Pause");

    return 0;
}

The output is

Creating 29 chairs in a classroom
Destroyng chairs in a classroom
Press any key to continue . . .
Destroyng chairs in a classroom

Yes, seems like the destructor gets called twice! Once upon adding to a vector, and second time upon the program finishing its execution.

The exact same thing happens when I try to use a class instead of a struct.

Can someone explain why this happens and what are the possible ways to accomplish my task correctly?

LPVOID
  • 306
  • 3
  • 17
  • 1
    Are you required to use a `char*`? `std::string` is the normal choice for string data and Just Works™ in regard to managing its memory. – chris Jun 13 '20 at 14:11
  • 1
    You'll need to provide a copy constructor that actually makes a copy of the data that the `chairs` member points to; otherwise, the `push_back` call (which invokes the default copy) will just make a copy of the pointer. Then, when any such copy is destroyed, the pointer will be invalidate. See https://en.wikipedia.org/wiki/Rule_of_three_(C%2B%2B_programming)#:~:text=The%20rule%20of%20three%20(also,copy%20assignment%20operator. – Adrian Mole Jun 13 '20 at 14:14
  • @chris I'm using `char*` because I need to store raw bytes – LPVOID Jun 13 '20 at 14:15
  • 2
    `std::vector`would be better. If using a `char*` is not a requirement there are much better ways. – drescherjm Jun 13 '20 at 14:16
  • @drescherjm I think `std::vector` would be of no use, since the destructor gets called anyway and I would delete the vector elements in it – LPVOID Jun 13 '20 at 14:20
  • You can make that a class member just like your `chairs` and it would not go out of scope until your class is destructed. This is preferred and will allow you to use the rule of 0 instead of 3 or 5. [https://en.cppreference.com/w/cpp/language/rule_of_three](https://en.cppreference.com/w/cpp/language/rule_of_three) – drescherjm Jun 13 '20 at 14:21
  • @LPVOID, Remember how I said the memory management Just Works™ [which applies to all standard containers, not just `std::string`]? They don't have the same error as your code, so the elements inside aren't going anywhere before the object containing them goes out of scope. The behaviour you're seeing is from your bug, not a general behaviour that containers have as well. – chris Jun 13 '20 at 14:24
  • @drescherjm The exact same thing happens when I try to use a class instead of a struct. The destructor gets called as soon as I put the entity into a vector. :/ – LPVOID Jun 13 '20 at 14:24
  • You are not instrumenting all of the calls so you don't see the copy constructor. This is related to the rule of 3/5/0. Your class is not handling it properly. See what happens if you use emplace_back in your example instead of push_back. – drescherjm Jun 13 '20 at 14:27
  • @chris Thank you for the link. I'll try to obey the Rule of three in my example and see if it works. I think I'm getting a bit more understanding now – LPVOID Jun 13 '20 at 14:33
  • @drescherjm I've just tried to use 'emplace_back', the behaviour didin't change. I'm applying the rule of three now and will post back if it helps – LPVOID Jun 13 '20 at 14:36
  • Thank you everyone, now I gained understanding of the copy constructor and it was exactly what was causing this behaviour. Would you post a beautiful answer so I can accept it? :) – LPVOID Jun 13 '20 at 14:54
  • @LPVOID -- *I think std::vector would be of no use, since the destructor gets called anyway and I would delete the vector elements in it* -- That is not the issue and isn't even an issue in your case. When the object is copied, the vector will get copied along with it. The vector is nothing more than `new[]`, but with member functions and doesn't make mistakes that you have now. – PaulMcKenzie Jun 13 '20 at 18:01

2 Answers2

0

@LPVOID

Using emplace_back(..) to create the object in place can help you avoid the double free or corruption error you are facing here.

m_classrooms.emplace_back(29)

However, it is a better practice to always follow the rule of 3/5/0 to not end up with a dangling pointer.

NULL
  • 1
  • 3
0

The Classroom class cannot be used in a std::vector<Classroom> safely because it has incorrect copy semantics. A std::vector will make copies of your object, and if the copy semantics have bugs, then you will see all of those bugs manifest themselves when you start using the class in containers such as vector.

For your class to have correct copy semantics, it needs to be able to construct, assign, and destruct copies of itself without error (those errors being things like memory leaks, double deletion calls on the same pointer, etc.)

The other thing missing from your code is that the size argument needs to be known within the class. Right now, all you've posted is an allocation of memory, but there is nothing that saves the size. Without knowing how many characters were allocated, proper implementation of the user-defined copy constructor and assignment operator won't be possible, unless that char * is a null-terminated string.


Having said that, there a multiple ways to fix your class. The easiest way is to simply use types that have correct copy semantics built into them, instead of handling raw dynamically memory yourself. Those classes would include std::vector<char> and std::string. Not only do they clean up themselves, these classes know their own size without having to carry a size member variable.

struct Classroom
{
    std::vector<char> chairs;

    Classroom() {} // default constructor
    Classroom(size_t size) : chairs(size)
    {
        std::cout << "Creating " << size << " chairs in a classroom" << std::endl;
    }
};

The above class will work without any further adjustments to it, since std::vector<char> has correct copy semantics already. Note that there is no longer a need for the destructor, since std::vector knows how to destroy itself.


If for some reason you had to use raw dynamically allocated memory, then your class has to implement a user-defined copy constructor, assignment operation, and destructor.

#include <algorithm>

struct Classroom
{
    size_t m_size;
    char* chairs;

    // Note we initialize all the members here.  This was a bug in your original code
    Classroom() : m_size(0), chairs(nullptr) 
    {} 

    Classroom(size_t size) : m_size(size), chairs(new char[size])
    {}

    Classroom(const Classroom& cRoom) : m_size(cRoom.m_size),
                                        chairs(new char[cRoom.m_size]) 

    {
       std::copy(cRoom.chairs, cRoom.chairs + cRoom.m_size, chairs);
    }

    Classroom& operator=(const Classroom& cRoom)
    {
       if ( this != &cRoom )
       {
          Classroom temp(cRoom);
          std::swap(temp.m_size, m_size);
          std::swap(temp.chairs, chairs);
       }
       return *this;
   }

   ~Classroom() { delete [] chairs; }
};

Note the usage of the member-initialization list when initializing the members of the class. Also note the usage of the copy / swap idiom when implementing the assignment operator.


The other issue that was corrected is that your default constructor was not initializing all of the members. Thus in your original class a simple one line program such as:

int main()
{
   Classroom cr;
}

would have caused issues, since in the destructor, you would have deleted an uninitialized chairs pointer.

After this, a std::vector<Classroom> should now be able to be safely used.

PaulMcKenzie
  • 34,698
  • 4
  • 24
  • 45
  • It would be great to also include the link that @drescherjm mentioned [https://en.cppreference.com/w/cpp/language/rule_of_three](https://en.cppreference.com/w/cpp/language/rule_of_three) – LPVOID Jun 14 '20 at 06:38