Search code examples
c++design-patternsnew-operatordelete-operator

How to realloc in c++?


The following code constitutes a MCVE, this reproduces the problem I want to ask about but it's not the real code. The real code is quite more complicated so that's why I wrote this for a demonstration of the problem.

The key feature I am looking for is to be able to grow a dynamically allocated array, please do not suggest using the stl because it's explicitly forbidden. This code is for educational purpose and thus there are restrictions.

#include <cstring>
#include <iostream>

class Value
{
    public:
        Value(int value = 0);
        Value(const Value &value);
        Value &operator =(const Value &other);
        ~Value();

        operator int() {return *m_absurdPointer;}

    private:
        int *m_absurdPointer;
};

Value::Value(int value) :
    m_absurdPointer(new int[1])
{
    *m_absurdPointer = value;
}

Value::Value(const Value &value)
{
    m_absurdPointer = new int[1];
    memcpy(m_absurdPointer, value.m_absurdPointer, sizeof(*m_absurdPointer));
}

Value &Value::operator =(const Value &other)
{
    m_absurdPointer = new int[1];
    memcpy(m_absurdPointer, other.m_absurdPointer, sizeof(*m_absurdPointer));

    return *this;
}

Value::~Value()
{
    delete[] m_absurdPointer;
}

class ValueArray
{
    public:
        ValueArray();
        ~ValueArray();

        void append(const Value &value);
        void show() const;

    private:
        Value *m_array;
        unsigned int m_capacity;
        unsigned int m_length;
};


ValueArray::ValueArray() :
    m_array(nullptr)
  , m_capacity(0)
  , m_length(0)
{
}

ValueArray::~ValueArray()
{
    delete[] m_array;
}

void
ValueArray::append(const Value &value)
{
    if (m_length >= m_capacity)
    {
        Value *newarray;
        unsigned int unitSize;

        unitSize = 1;       
        newarray = new Value[m_capacity + unitSize];

        if ((m_capacity > 0) && (m_array != nullptr))
            memcpy(newarray, m_array, m_capacity * sizeof(*m_array));
        delete[] m_array;

        m_array = newarray;
        m_capacity += unitSize;
    }
    m_array[m_length++] = value;
}

void
ValueArray::show() const
{
    for (size_t i = 0 ; i < m_length ; ++i)
        std::cout << static_cast<int>(m_array[i]) << std::endl;
}

int
main(void)
{
    ValueArray example;

    for (int i = 0 ; i < 10 ; ++i)
        example.append(Value(i));
    example.show();

    return 0;
}

It causes as you can see a double free issue, because the delete[] m_array; calls the destructor of the class Value after it has copied the values to the re-newed array.

I tried to do this with malloc()/realloc() but I need the destructor of Value() to be called so new is mandatory because I can't use free().

How to prevent this?, if I remove the delete[] m_absurdPointer; the double free would be gone of course but there would be a memory leak.


Solution

  • You basically want to implement an own vector class, right?

    OK, first things first: As far as I know you cannot grow previously allocated memory. At least not with the standard allocator.

    So you need to allocate a new, larger chunk of memory.

    You can do this the standard way, using new:

    Type * newdata = new Type[size];
    

    In this case the constructor of the class Type will be called for each new element, which is size times.

    To get your old data into that new array you need to copy or move it there:

    for (size_t it = 0; it < oldsize; ++it) {
      newdata[it] = olddata[it];
      // newdata[it] = std::move(olddata[it]);
    }
    

    This is what std::copy resp. std::move are doing. (You could also use std::swap inside a loop.)

    For that to work the Type class needs both a default constructor and a valid implementation of copy or move assignment.

    You're using memcpy. In C++, this is generally a bad idea: Your implemented assignment operator isn't called, Therefore both the objects in your old array and the raw copies are using the same pointer, which is why you get that double free, obviously.

    You could also allocate raw memory and use placement new to copy or move construct the new objects from the old ones:

    void * memory = new char[size * sizeof(Type)];
    for (size_t it = 0; it < oldsize; ++it) {
      new (memory + it * sizeof(Type)) Type(olddata[it]); // copy
    }
    

    The above is only an example, for real code you need to consider alignment, too.

    Finally, I'm sure you can somehow trick the default allocator to free your (old) memory without destructing the objects within, this allowing you to use the raw copy memcpy made. Though this would be a hack and could break on complex classes, it's not the C++ way of doing this.

    The idiomatic way is to copy or move the old objects to the new storage (with either assignment or construction).