Если вы освободите массив, если выделение этого массива вызвало исключение?

У меня есть потенциально неустойчивый класс, который написал кто-то другой, и мне нужно создать массив объектов этого класса. Я упомянул, что класс нестабилен, поэтому он может иногда генерировать исключение в конструкторе по умолчанию. У меня нет доступа к исходному коду, только скомпилированные двоичные файлы.

Когда я распределяю динамический массив этих типов объектов с помощью new, существует вероятность того, что один из этих плохих объектов может вызвать исключение. Это бросает пользовательское исключение, а не std::bad_alloc. Во всяком случае, мне нужно, чтобы программа восстановилась из исключения и просто продолжала прерываться, хотя и устанавливала некоторые флаги ошибок, а что нет. Я думаю, что я должен delete память, связанную с массивом, чтобы предотвратить утечку памяти.

Мое рассуждение состоит в том, что если класс генерирует исключение, строящее элемент где-то посередине массива, этот элемент не будет правильно построен, и все будущие элементы будут остановлены за исключением исключения, но предыдущие элементы будут правильно построенный, поскольку это произошло до того, как было выброшено исключение. Мне интересно, хорошая идея назвать delete в catch (...) { }? Как я могу решить проблему утечки памяти?

Badclass* array = nullptr;

try {
  array = new Badclass[10];  // May throw exceptions!
} catch (...) {
  delete[] array;
  array = nullptr;
  // set error flags
}

Так я визуализирую это в памяти. Это правильно?

 array         0    1    2    3   4    5 6 7 8 9
 ___          __________________________________
| ---------->| :) | :) | :) | :) | :( | | | | | |
|___|        |____|____|____|____|____|_|_|_|_|_|

Ответы

Ответ 1

Чтобы ответить на последний вопрос:

Как я могу решить проблему утечки памяти?

Нет утечки памяти. Утечка произошла бы только в том случае, если бы BadClass сам динамически выделял контент и никогда не освобождал его в своем деструкторе. Поскольку мы не обращаем внимания на вашу реализацию BadClass, а не на то, чтобы догадываться, что до вас. Единственный способ - new BadClass[N]; утечка памяти сама по себе, если она завершается, и вы позже отбрасываете единственную ссылку на нее, которую вы вручную управляете (array).

Массив, динамически распределенный, бросая внутри одного из конструкторов для элементов в нем, будет (а) удалять деструкторы в противоположном порядке для уже построенных элементов, (б) освобождать выделенную память и, наконец, (c) выполнять фактический бросок на ближайшего обработчика catch (или обработчика по умолчанию, когда его нет).

Поскольку происходит бросок, присваивание результирующего указателя массива никогда не возникает, и поэтому не требуется delete[].

Лучший пример:

#include <iostream>

struct A 
{
    static int count;
    int n;

    A() : n(++count)
    {
        std::cout << "constructing " << n << '\n';
        if (count >= 5)
            throw std::runtime_error("oops");
    }

    ~A()
    {
        std::cout << "destroying " << n << '\n';
    }
};

int A::count;

int main()
{
    A *ar = nullptr;
    try
    {
        ar = new A[10];
    }
    catch(std::exception const& ex)
    {
        std::cerr << ex.what() << '\n';
    }
}

Выход

constructing 1
constructing 2
constructing 3
constructing 4
constructing 5
destroying 4
destroying 3
destroying 2
destroying 1
oops

Обратите внимание, что поскольку конструкция элемента '5' никогда не завершается, его деструктор не запускается. Тем не менее, члены, которые были успешно построены, разрушены (не продемонстрированы в приведенном выше примере, но это забавное упражнение, если вы для этого заняты).

Все это говорит, что использовать интеллектуальные указатели.

Ответ 2

В следующей строке кода:

array = new Badclass[10];  

new Badclass[10] оценивается new Badclass[10]. Если это генерирует исключение, выполнение не достигает назначения. array сохраняет свое предыдущее значение, которое равно nullptr. Он не имеет никакого эффекта для вызова delete на nullptr.

Вопрос из раздела комментариев:

Такое поведение основано на том же принципе, что и разматывание стека?

Раздел "Обработка исключений" в стандарте помогает нам понять, что происходит, когда исключение выбрасывается во время выделения.

18 Обработка исключений [кроме]
...
18.2 Конструкторы и деструкторы [except.ctor]

1. Когда управление переходит от точки, в которой исключение отправляется обработчику, деструкторы вызываются процессом, указанным в этом подпункте, называемым распаковкой стека.
...
3. Если инициализация или уничтожение объекта, отличного от делегирования конструктора, завершается исключением, деструктор вызывается для каждого из объектов прямых подобъектов, а для объекта - объекта объекта виртуального базового класса, завершение которого завершено и чья деструктор еще не приступил к исполнению, за исключением того, что в случае уничтожения варианты членов подобного типа не уничтожаются. Субобъекты уничтожаются в обратном порядке завершения их строительства. Такое разрушение секвенируется до ввода обработчика функции-try-блока конструктора или деструктора, если таковые имеются.

Ответ 3

Нет необходимости называть delete в случае исключения в:

array = new Badclass[10];  // May throw exceptions!

Нет утечки памяти.

В качестве ссылки читайте о новом выражении на cppreference:

Если инициализация завершается путем исключения исключения (например, из конструктора), если новое выражение выделяет какое-либо хранилище, оно вызывает соответствующую функцию освобождения: оператор delete для типа без массива, оператор delete [] для типа массива.

Поэтому в нем четко указано, что delete[] вызывается автоматически, и вам не нужно его вызывать.

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