J. Lengel
J. Lengel

Reputation: 588

C++ Dynamic Member Arrays Deleted Right Before Destructor Called

I'm working on an AI project and have started to implement a NeuralNetwork class. I just want to get something basic down so I used some malloc statements with some placement news and finally delete[]s.

However, once the NeuralNetwork object (created on the stack in the main function) is about to be deleted (I set a breakpoint at the start of the destructor), my arrays seem to have been prematurely deleted (value 0xcccccccc) and the delete[] statements therefore throw access violations.

Through further investigation I found out that this deleting happens right between the last Vector object being destructed and the start of the destructor of my NeuralNetwork object being called.

I made sure to implement both copy constructors and assignment operators to make sure no copying was taking place without me noticing.

I'm really baffled with this problem and hope that someone can catch my mistake. Source code will follow:

NeuralNetwork::NeuralNetwork(const std::initializer_list<size_t>& l):
    m_size(l.size() - 1),
    m_weights(static_cast<Matrix<double>*>(malloc(sizeof(Matrix<double>) * m_size))),
    m_biases(static_cast<Vector<double>*>(malloc(sizeof(Vector<double>) * m_size)))
{
    size_t index = 0;
    auto itr = l.begin();

    for (auto next = itr + 1; next != l.end(); ++next, ++itr, ++index)
    {
        new (m_weights + index) Matrix<double>(*next, *itr);
        new (m_biases + index) Vector<double>(*next);
    }
}

NeuralNetwork::NeuralNetwork(const NeuralNetwork& nn) :
    m_size(nn.m_size),
    m_weights(static_cast<Matrix<double>*>(malloc(sizeof(Matrix<double>)* m_size))),
    m_biases(static_cast<Vector<double>*>(malloc(sizeof(Vector<double>)* m_size)))
{
    for (size_t index = 0; index < m_size; ++index)
    {
        new (m_weights + index) Matrix<double>(nn.m_weights[index]);
        new (m_biases + index) Vector<double>(nn.m_biases[index]);
    }
}

NeuralNetwork::NeuralNetwork(NeuralNetwork&& nn) noexcept :
    m_size(nn.m_size),
    m_weights(nn.m_weights),
    m_biases(nn.m_biases)
{
    nn.m_size = 0;
    nn.m_weights = nullptr;
    nn.m_biases = nullptr;
}

NeuralNetwork::~NeuralNetwork()
{
    delete[] m_weights; // exception thrown here, value is 0xcccccccc, nullptr
    delete[] m_biases;
}

Main code:

int main()
{
    NeuralNetwork nn{ 2, 1 };

    Vector<double> input(2);
    input.Get(0) = 0.5;
    input.Get(1) = 0.25;

    Vector<double> output = nn.Forward(input); // just does the math, nothing special

    for (size_t i = 0; i < output.GetSize(); ++i)
        std::cout << output.Get(i) << " ";

    std::cout << std::endl;
}

In case any important source code is missing please let me know, thanks!

Upvotes: 1

Views: 105

Answers (3)

tevemadar
tevemadar

Reputation: 13195

As you are using C++11 features already, you can also use std::vector::emplace_back(), which will deal with placement new internally.

Example:

#include <iostream>
#include <initializer_list>
#include <vector>

template<class T> class Matrix {
public:
    Matrix() {std::cout << "Matrix() " << this << std::endl;}
    Matrix(int width,int height):w(width),h(height) {std::cout << "Matrix(" << w << "x" << h << ") " << this << std::endl;}
    ~Matrix() {std::cout << "Matrix(" << w << "x" << h << ") " << this << " goodbye" << std::endl;}
private:
    int w,h;
};

class NN {
public:
    NN()=default;
    NN(const std::initializer_list<size_t> &l);
private:
    std::vector<Matrix<double>> m_weights;
};

NN::NN(const std::initializer_list<size_t> &l) {
    m_weights.reserve(l.size()-1); // or deal with move constructors
    auto itr = l.begin();
    for (auto next = itr + 1; next != l.end(); ++next, ++itr)
    {
        m_weights.emplace_back(*next, *itr);
    }
}

int main() {
    NN test{2,3,3,2};
    return 0;
}

Output (from https://ideone.com/yHGAMc):

Matrix(3x2) 0x5638f59aae70
Matrix(3x3) 0x5638f59aae78
Matrix(2x3) 0x5638f59aae80
Matrix(3x2) 0x5638f59aae70 goodbye
Matrix(3x3) 0x5638f59aae78 goodbye
Matrix(2x3) 0x5638f59aae80 goodbye

So the default constructor was not involved and objects were destructed properly.

Upvotes: 0

Phil1970
Phil1970

Reputation: 2623

You should never write such code:

  • You should never have to use malloc/free in a C++ program.
  • Allocation and desallocation should match.
  • Dynamic memory allocation has surely more overhead that default initialization you try to avoid.
  • Your code would miserably failed if initializer list is empty.
  • Code has memory leaks.
  • If you define a copy constructor, then you should also define assignment operator (same for move constructor).

Standard library already do most relavant optimization. For example,, for a std::vector the constructor of an item will be only called when you call emplace_back.

You should really write standard code. It does not worth the trouble to write bugged code for marginal performance improvement.

Your class declaration should really look something like:

class NeuralNetwork
{
public:
    NeuralNetwork(const std::initializer_list<size_t>& l);
    // Other constructors as appropriate here…

private:
    std::vector<Matrix<double>> m_weights;
    std::vector<Vector<double>> m_biases;
};

And constructor should look similar to that (code not tested):

NeuralNetwork::NeuralNetwork(const std::initializer_list<size_t>& l):
{
    if (l.empty()
    {
        // might assert in debug or throw an exception...
        return;
    }

    m_weights.reserve(m_size);
    m_biases.reserve(m_size);

    auto itr = l.begin();
    for (auto next = itr + 1; next != l.end(); ++next, ++itr, ++index)
    {
        m_weights.emplace(*next, *itr);
        m_biases.emplace(*next);
    }
}

Other constructors, assignment operators and destructors should be easier to implement, more robust and performance very similar.

Upvotes: 1

Victor Padureanu
Victor Padureanu

Reputation: 614

There is a big difference between malloc/free and new/delete and new[] / delete[]

malloc will allocate an unformated chunk of memory and free will free it

new will allocate and initialize that region and delete will call the destructor

sometimes it might work to use malloc and delete but it's a bad idea

new[] will also keep a few extra info before the the returned memory to know how many objects need to be deleted

Usefull links: https://www.geeksforgeeks.org/placement-new-operator-cpp/

Upvotes: 3

Related Questions