Динамическая память и исключения конструктора

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

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


Учитывая этот пример кода:

class A
{
private:
    B b
    C *c;    //classes B, C & D omitted for brevity as not really relevant
    D d;
public
    A(int x, int y, int z)
};

A::A(int x, int y, int z)
try
    : b( x )
    , c( new C(y) )
    , d( z )
{
    //omitted
}
catch(...)
{
    //omitted
}

Что происходит в этих случаях:

  1. Инициализация b вызывает исключение.
  2. Инициализация c вызывает исключение.
  3. Инициализация d вызывает исключение.

В частности, я хочу знать как минимум:

  • что вызовет / может вызвать утечку памяти из new C(y). Думаю, всего 3? (см. здесь)
  • не могли бы вы просто delete b в улове? Опасно ли в случаях 1 и 2?

Очевидно, что безопаснее всего сделать c умный указатель. Но если на данный момент не принимать во внимание этот вариант, каков наилучший курс действий?

Безопасно ли установить c в NULL в инициализаторе, а затем разместить вызов new в теле конструктора?

Тогда это будет означать, что delete c должен быть помещен в уловку на случай, если что-то еще бросит в тело конструктора? Есть ли при этом проблемы с безопасностью (например, если это сам c = new C(y); выбрасывает)?


person DMA57361    schedule 05.07.2010    source источник
comment
Вы не можете зависеть от установки c в NULL, потому что, если b вызывает исключение, c не было бы установлено в NULL в этот момент. Как вы сказали, используйте умный указатель.   -  person 5ound    schedule 05.07.2010
comment
@ 5ound - очень хороший момент, о котором я бы не подумал.   -  person DMA57361    schedule 05.07.2010


Ответы (3)


Функциональные блоки try / catch не одобряются, так же как и goto - могут быть некоторые угловые случаи, когда они имеют смысл, но их лучше избегать: когда объект не может быть построен, лучшее, что вы можете сделать, это потерпеть неудачу и быстро терпят неудачу.

По вашим конкретным вопросам, когда в конструкторе создается исключение, все полностью построенные подобъекты будут уничтожены. Это означает, что в случае b он будет уничтожен, в случае c, поскольку это необработанный указатель, ничего не будет сделано. Самым простым решением является изменение c на интеллектуальный указатель, который обрабатывает выделенную память. Таким образом, если d выдает ошибку, интеллектуальный указатель будет уничтожен, а объект освобожден. Это не связано с блоком try / catch, а скорее с тем, как работают конструкторы.

Также, как правило, небезопасно удалять указатель из блока catch, поскольку нет гарантии фактического значения указателя до выполнения инициализации. То есть, если b или c выбрасывают, может случиться так, что c не равно 0, но также не является допустимым указателем, и его удаление будет неопределенным поведением. Как всегда, бывают случаи, как будто у вас есть гарантия, что ни b, ни c не сработают, и если предположить, что bad_alloc - это не то, от чего вы обычно восстанавливаетесь, тогда это может быть безопасно.

Обратите внимание, что если вам необходимо сохранить указатель с необработанным указателем по какой-либо конкретной причине, лучше инициализировать его значением 0 в списке инициализации, а затем создать объект внутри строительного блока, чтобы избежать этой проблемы. Помните также, что при хранении более одного (необработанного) ресурса непосредственно в классе очень сложно гарантировать отсутствие утечки ресурсов - если первый ресурс создается и назначается, а второй выходит из строя, деструктор не будет вызываться, и ресурс будет протекать. Опять же, если вы можете использовать умный указатель, у вас будет на одну проблему меньше.

person David Rodríguez - dribeas    schedule 05.07.2010
comment
Да, кажется, чем больше я думаю и читаю об этом, тем больше они мне не помогают. - person DMA57361; 05.07.2010
comment
Ваши утверждения о блоках try / catch не одобряются, так же как и goto ... - это просто ерунда. try / catch - это языковая функция, которая используется для написания кода с надлежащей обработкой ошибок в контексте исключений. Как еще вы ожидаете обработки исключений? - person hkaiser; 05.07.2010
comment
@hkaiser: здесь произошло недоразумение, что почему-то осуждают, так это блоки try / catch на уровне функций: void f() try { ... } catch () {}. Эта функция была добавлена ​​в язык, но пока неизвестна и не рекомендуется. Я прояснил намерение - или, по крайней мере, попытался - сейчас. Регулярные блоки try / catch внутри функций - это, как вы правильно заявили, языковая функция для обработки ошибок. Я постараюсь найти ссылки из хорошо известных источников, когда вернусь домой. Это первый: GOTW # 66 - person David Rodríguez - dribeas; 05.07.2010
comment
@David - эта ссылка в вашем комментарии очень полезна - и вот цитата из раздела «мораль» (между 3 и 4), которая, как мне кажется, дает ответ: ... или new [] it в теле конструктора, где он можно безопасно очистить с помощью локального блока try или иным образом. Для меня это звучит как лучшее решение (исключая интеллектуальные указатели) - выполнить каждый требуемый new в теле конструктора внутри его собственного, вложенного, попробовать / поймать - тогда уловы могут очистить все предыдущие усилия, а затем что-то выбросить. - person DMA57361; 05.07.2010
comment
@ DMA57361: лучшее решение - использовать интеллектуальный менеджер для каждого из ресурсов: интеллектуальные указатели для новых объектов, std::vector для динамически выделяемых массивов и т.д. молоток для проведения операции, он может сработать, но он излишне сложен и вряд ли проработает так долго (обслуживание ...) - person Matthieu M.; 06.07.2010
comment
@Matthiew M - О, я полностью согласен. Я просто пытался лучше понять то, что кажется (или, точнее, является) довольно сложной областью. И в какой-то момент какой-то класс должен будет решить эту проблему? Умные указатели и тому подобное, на которые мы полагаемся, должны решить эту проблему, потому что они не могут использовать эти конструкции сами - но я предполагаю, что это будет в основном внутри ядра библиотек, где большинству из нас не нужно особо беспокоиться об этом... - person DMA57361; 06.07.2010
comment
@ DMA57361: * ... какой-то класс будет работать над этой проблемой ... * Не совсем, умные указатели сами по себе довольно просты и не нуждаются в этом. Ресурс создается извне, если это создание не удается, он автоматически очищается. Затем он передается интеллектуальному указателю, и интеллектуальный указатель только копирует указатель - не может выбросить. Если позже что-нибудь выкинет, это не проблема. Класс, реализующий RAII, уже полностью построен, может быть уничтожен. Деструктор может выполнить очистку самым простым способом. - person David Rodríguez - dribeas; 06.07.2010

Вы не можете ничего делать в обработчике функционально-пробного блока, кроме как переводить одно исключение в другое. Вы не можете предотвратить создание исключения. Вы ничего не можете сделать с учениками. Так что нет, вы не можете сделать delete c в блоке функции-попытки конструктора.

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

и

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

(15.3 [except.handle] из Стандарта C ++)

person atzz    schedule 05.07.2010
comment
Итак, в основном объект уничтожается (фактически) как часть раскрутки, которая происходит до начала catch. Итак, в catch текущий объект не существует; вы не можете пытаться очистить объект, которого нет. - person DMA57361; 05.07.2010
comment
@ DMA57361 - да. Насколько я понимаю, function-try-block был добавлен в язык только с целью преобразования исключений в конструкторах, чтобы облегчить совместное использование классов из независимых библиотек. Также вы можете положить туда логи. Вот и все ... - person atzz; 05.07.2010

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

Вы правы в том, что утечка памяти произойдет только тогда, когда d вызовет исключение, и в этом случае внешнюю catch необходимо очистить.

Если сам new C выдает ошибку, объект никогда не создается, поэтому вам не нужно беспокоиться об его удалении.

person casablanca    schedule 05.07.2010
comment
Есть какие-нибудь предложения по безопасной очистке улова? У меня такое чувство, что это невозможно, и вы остаетесь либо с возможной утечкой памяти, либо с риском удаления неинициализированного указателя. - person DMA57361; 05.07.2010
comment
В каком-то смысле вы правы - это приведет к неловким ситуациям, и вы не всегда можете гарантировать чистое решение. Если b, c и d все выдают разные исключения, вы можете выяснить, где именно произошел сбой, и выполнить соответствующую очистку. - person casablanca; 05.07.2010