Проблема с деструктором при удалении [] - PullRequest
0 голосов
/ 26 июня 2018

Для программы, которую я пишу, я написал простой класс-обертку для массива (идея в том, что он должен быть фиксированного размера. Я знаю, что мог бы просто использовать std :: vectors) И у меня есть проблема при удалении массива.
Это мои конструкторы:

template <class T>
Array<T>::Array(size_t size): m_data(new T[size]), m_size(size)
{}
template <class T>
Array<T>::Array(const std::vector<T> &vec): m_size(vec.size())
{
    std::allocator<T> a;
    m_data = a.allocate(m_size);
    for(int i = 0; i<m_size; i++)
    {
        new (m_data+i) T(vec[i]);
    }
}

А это мой деструктор:

template <class T>
Array<T>::~Array()
{
    delete[] m_data;
}

Я использовал valgrind, чтобы попытаться выяснить, что происходит, но это не помогло.

==20840== Invalid read of size 8
==20840==    at 0x10ABB8: sapph_dijkstra::Array<sapph_dijkstra::Node>::~Array() (in /home/sapphie/dijkstra/test)
==20840==    by 0x1091CF: sapph_dijkstra::MinHeap::~MinHeap() (minheap.h:40)
==20840==    by 0x109021: main (test.c:20)
==20840==  Address 0x5b21318 is 8 bytes before a block of size 400 alloc'd
==20840==    at 0x4C3017F: operator new(unsigned long) (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==20840==    by 0x109F99: __gnu_cxx::new_allocator<sapph_dijkstra::Node>::allocate(unsigned long, void const*) (new_allocator.h:111)
==20840==    by 0x10AA36: sapph_dijkstra::Array<sapph_dijkstra::Node>::Array(std::vector<sapph_dijkstra::Node, std::allocator<sapph_dijkstra::Node> > const&) (in /home/sapphie/dijkstra/test)
==20840==    by 0x10A232: sapph_dijkstra::MinHeap::MinHeap(std::vector<sapph_dijkstra::Node, std::allocator<sapph_dijkstra::Node> > const&) (in /home/sapphie/dijkstra/test)
==20840==    by 0x108FD1: main (test.c:20)
==22059== Invalid free() / delete / delete[] / realloc()
==22059==    at 0x4C3173B: operator delete[](void*) (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==22059==    by 0x10ABA4: sapph_dijkstra::Array<sapph_dijkstra::Node>::~Array() (in /home/sapphie/dijkstra/test)
==22059==    by 0x1091CF: sapph_dijkstra::MinHeap::~MinHeap() (minheap.h:40)
==22059==    by 0x109021: main (test.c:20)

(я вырезал несколько идентичных сообщений об ошибках)
В распечатках, которые я сделал, я выяснил, что адрес, который я выделил, равен 0x5b21320, так что адрес, показанный valgrind, на самом деле составляет 8 байтов до этого. Но я не понимаю почему. Я, кажется, не получить доступ к этому.
Я скучаю по чему-то тривиальному?
Я понимаю, что это, вероятно, слишком сложное решение для проблемы, где я мог бы просто использовать стандартный вектор, и я, вероятно, собираюсь изменить это. Но сейчас мне больше всего любопытно.

Ответы [ 2 ]

0 голосов
/ 26 июня 2018

Вы не можете удалить с помощью delete[], если память не была инициализирована с помощью new[]. Это одна из ситуаций (из очень, очень, очень нескольких ситуаций), когда правильно вызывать деструктор объекта:

template <class T>
Array<T>::~Array()
{
    for(size_t i = 0; i < m_size; i++) {
        m_data[i].~T();
    }
    allocator.deallocate(m_data);
}

Есть несколько других изменений, которые вы, вероятно, должны сделать:

  • Объект распределителя должен быть членом объекта Array, а не локальным объектом в его конструкторе. Хотя большинство распределителей не имеют состояния, некоторые таковы, и необходимость создания локальной копии распределителя только внутри конструктора означает, что ваш распределитель потеряет свое состояние.
  • Вы должны принять исполнительное решение относительно того, хотите ли вы уничтожать объекты в том же порядке, в каком delete[] уничтожает объекты. В вашей ситуации вам нужно указать это самостоятельно, изменив порядок цикла for в деструкторе.

Вот как это будет выглядеть:

template <class T>
Array<T>::~Array()
{
    for(size_t i = 0; i < m_size; i++) {
        m_data[m_size - i - 1].~T();
    }
    allocator.deallocate(m_data);
}
  • Я бы также убедился, что вы избегаете использования int для всего, что связано с размерами этого массива. Нужно привести аргумент, чтобы отдать предпочтение int64_t, а не size_t (хотя это оторвется от поведения STL и является решением, которое нужно принимать осторожно), но вы определенно не хотите ограничивать себя использование 32-разрядного числа со знаком или меньше (что int в большинстве сред).

Относительно выбора не использовать delete:

delete ожидает, что переданный ему указатель будет отдельным дискретно размещенным объектом. Объекты, созданные с использованием размещения - new, не размещаются таким образом. Учтите следующее:

struct point {
    int32_t x, y;
};

int main() {
    char memory[1024];
    size_t size = 128;
    point * arr = reinterpret_cast<point*>(memory);
    for(size_t i = 0; i < size; i++) {
        new(memory + i) point();
    }
    for(size_t i = 0; i < size; i++) {
        arr[i].x = 5;
        arr[i].y = 10;
    }
    for(size_t i = 0; i < size; i++) {
        //undefined behavior, we're deleting objects that weren't allocated on the heap!
        delete (arr + (size - i - 1));
    }
}

В этом примере вообще нет динамического распределения памяти; вся используемая память выделяется в стеке. Если бы мы использовали delete в этой ситуации, мы бы удалили указатели на память в стеке, и может произойти любое количество (неопределенных) вещей, скорее всего, включая сбой нашей программы. Вызов деструктора позволяет объекту очистить свои составляющие объекты без необходимости (в данном случае ненужного) освобождения памяти.

for(size_t i = 0; i < size; i++) {
    //Correct behavior, doesn't delete stack memory
    arr[size - i - 1].~point();
}

Даже в ситуации, когда эта память была выделена в куче, мы все равно будем удалять указатели, которые не были явно выделены кодом. Это запрещено языком, и легко понять, почему: одно освобождение исходной памяти стирает весь фрагмент выделенной памяти, нет необходимости освобождать отдельные фрагменты.

struct point {
    int32_t x, y;
};

int main() {
    char * memory = new char[1024];
    size_t size = 128;
    point * arr = reinterpret_cast<point*>(memory);
    for(size_t i = 0; i < size; i++) {
        new(memory + i) point();
    }
    for(size_t i = 0; i < size; i++) {
        arr[i].x = 5;
        arr[i].y = 10;
    }
    for(size_t i = 0; i < size; i++) {
        //Still UB: only the first object is a discrete allocation, the rest are part of
        //that original allocation. This attempts to deallocate the same chunk of memory 128 times
        //delete (arr + (size - i - 1));

        //This is correct
        arr[size - i - 1].~point();
    }
    //We do need to deallocate the original memory allocated, but we only do this once.
    delete[] memory;
}
0 голосов
/ 26 июня 2018

Вы смешиваете new и delete[], что является неопределенным поведением. std::allocator::allocate использует operator new, а не operator new[], поэтому вы должны вызвать delete.

Поскольку вы смешиваете то, как вы распределяете хранилище, вам нужно следить за тем, каким образом вы распределили его, чтобы вы могли правильно освободить его.

Вы можете избежать всего этого, просто указав std::vector в качестве члена данных класса. Тогда ваши конструкторы станут

template <class T>
Array<T>::Array(size_t size): m_data(size) {}

template <class T>
Array<T>::Array(const std::vector<T> &vec): m_data(vec) {}

И вы получаете то преимущество, что вам не нужно хранить элемент размера, поскольку вектор делает это за вас.

...