Что произойдет при вызове исключения в конструкторе класса

Обновлено: 03.05.2024

This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

  • Open with Desktop
  • View raw
  • Copy raw contents Copy raw contents

Copy raw contents

Copy raw contents

Возврат кода ошибки

- Ошибку можно проигнорировать

- Делает код громозким

Поддержка со стороны C++

- При неправильном использовании могут усложнить программу

Что такое исключительная ситуация?

Ошибка которую нельзя обработать на данном уровне и игнорирование которой делает дальнейшую работу программы бессмысленной.

Гарантии безопасности исключений (exception safety)

  1. Гарантировано искючений нет (No-throw guarantee)

Операции всегда завершаются успешно, если исключительная ситуация возникла она обрабатывается внутри операции.

Также известна как коммит ролбек семантика (commit/rollback semantics). Операции могут завершиться неудачей, но неудачные операции гарантированно не имеют побочных эффектов, поэтому все данные сохраняют свои исходные значения.

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

Поиск подходящего обработчика

  1. Поиск подходящего обработчика идет в порядке следования обработчиков в коде
  2. Полного соответствия типа не требуется, будет выбран первый подходящий обработчик
  3. Если перехватывать исключение по значению, то возможна срезка до базового класса
  4. Если наиболее общий обработчик идет раньше, то более специализированный обработчик никогда не будет вызван
  5. Три точки - перехват любого исключения

Исключения ОС - не исключения С++, например, деление на ноль. Для их обработки нужно использовать средства предоставляемые конкретной платформой

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

Если подходящий обработчик не был найден вызывается стандартная функция terminate.

Вызывает стандартную функцию С - abort.

abort - аварийное завершение программы, деструкторы объектов вызваны не будут.

Поведение terimnate можно изменить установив свой обработчик функцией set_terminate.

Где уместен catch (. )?

Только в main, для того, чтобы поймать необработанное исключение, чтобы избежать вызов terminate и таким образом завершить работу с вызовом деструкторов.

noexcept говорит компилятору, что функция не выбрасывает исключений - это позволяет компилятору генерировать более компактный код, но если фактически исключение было выброшено, то будет вызвана функция terminate.

Исключения в деструкторе

Исключение покинувшее деструктор во время раскрутки стека или у глобального/статического объекта приведет к вызову terminate.

Начиная с С++11 все деструкторы компилятором воспринимаются как помеченные noexcept - теперь исключения не должны покидать деструктора никогда.

Исключения в конструкторе

Клиент либо получает объект в консистентном состоянии, либо не получает ничего.

Для полностью сконструированных на момент выброса исключения объектов будут вызваны деструкторы, память выделенная под объект будет корректно освобождена, но поскольку объект не был полностью сконструирован, то деструктор вызван не будет.

Одним из преимуществ механизма исключения является то, что выполнение вместе с данными об исключении переходит непосредственно из оператора, который создает исключение, в первую инструкцию Catch, которая его обрабатывает. Обработчик может иметь любое количество уровней вверх в стеке вызовов. Функции, которые вызываются между оператором try и оператором throw, не обязательно должны знать какие-либо сведения о возникшем исключении. Однако они должны быть спроектированы таким образом, чтобы они могли выйти из области "непредвиденно" в любой момент, где исключение может быть распространено ниже, и сделать это без выхода из частично созданных объектов, утечки памяти или структур данных, которые находятся в непригодных для использования состояниях.

Основные методы

Независимо от того, как функция обрабатывает исключение, чтобы гарантировать, что оно является "типобезопасным", оно должно быть спроектировано согласно следующим основным правилам.

Простота сохранения классов ресурсов

При инкапсулировании управления ресурсами вручную в классах используйте класс, который не выполняет никаких действий, кроме управления одним ресурсом. Благодаря простоте использования класса вы снижаете риск возникновения утечек ресурсов. Используйте смарт-указатели , если это возможно, как показано в следующем примере. Этот пример намеренно искусственно и упрощен, чтобы выделить различия при shared_ptr использовании.

Использование идиомы RAII для управления ресурсами

Чтобы обеспечить безопасность исключений, функция должна гарантировать, что объекты, выделенные с помощью malloc или new , уничтожаются, и все ресурсы, такие как дескрипторы файлов, закрываются или освобождаются, даже если возникает исключение. Функция получения ресурсов — инициализация (RAII) идиома связывает такие ресурсы с сроком службы автоматических переменных. Если функция выходит за пределы области действия, либо путем возвращения обычно или из-за исключения, вызываются деструкторы для всех полностью сформированных автоматических переменных. Объект-оболочка RAII, такой как интеллектуальный указатель, вызывает соответствующую функцию DELETE или Close в своем деструкторе. В коде, защищенном с исключением, очень важно немедленно передавать владение каждым ресурсом объекту RAII. Обратите внимание, что vector классы, string make_shared ,, fstream и схожие по себе обрабатывали ресурсы для получения. Однако традиционные shared_ptr конструкции являются специальными, unique_ptr так как получение ресурса выполняется пользователем, а не объектом. Следовательно, они считаются освобождением ресурсов , но их можно считать RAII.

Три гарантии исключений

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

Гарантия No — сбой

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

Как строгая гарантия, так и Базовая гарантия основываются на предположении, что деструкторы являются неуспешными. Все контейнеры и типы в стандартной библиотеке гарантируют, что их деструкторы не вызывают исключение. Существует также и обратное требование: Стандартная библиотека требует, чтобы определяемые пользователем типы, например, в качестве аргументов шаблона, не создавали деструкторы без вызова.

Строгая гарантия

Строгая гарантия указывает, что если функция выходит за пределы области действия из-за исключения, она не будет приводить к утечке памяти, а состояние программы не будет изменено. Функция, обеспечивающая строгую гарантию, по сути, является транзакцией, которая имеет семантику фиксации или отката: либо полностью завершается успешно, либо не оказывает никакого воздействия.

Базовая гарантия

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

Классы, защищенные с исключением

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

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

Для управления всеми ресурсами используйте смарт-указатели или другие оболочки типа RAII. Избегайте функций управления ресурсами в деструкторе класса, так как деструктор не будет вызываться, если конструктор выдаст исключение. Однако если класс является выделенным диспетчером ресурсов, который управляет только одним ресурсом, то для управления ресурсами допустимо использовать деструктор.

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

Определите, следует ли сохранять все состояния класса в члене данных, заключенном в интеллектуальный указатель, особенно если класс имеет концепцию "инициализация, которая может быть неудачной". Хотя C++ допускает неинициализированные элементы данных, он не поддерживает неинициализированные или частично инициализированные экземпляры класса. Конструктор должен быть успешно выполнен или завершился ошибкой. Если конструктор не выполняется до завершения, объект не создается.

Не разрешать исключения в escape-последовательности из деструктора. Базовый аксиома C++ заключается в том, что деструкторы никогда не должны разрешать исключение для распространения стека вызовов. Если деструктор должен выполнить операцию, вызывающую исключение, он должен быть выполнен в блоке try catch и проглотить исключение. Стандартная библиотека обеспечивает эту гарантию для всех деструкторов, которые он определяет.

Говорят, что конструктор конструктор и деструктор класса не должны вырабатывать исключения?
В тоже время, я видел огромное количество кода и даже примеров из книг, где в конструкторе выделяется память при помощи оператора `new`, но ведь даже он может вырабатывать исключение `bad_alloc` — выходит, что весь этот код потенциально опасный? Как с этим бороться?

Исключения, ровно как и оператор return прерывают поток выполнения команд функции, из системного стека выбираются объекты (такие как локальные переменные) и для них вызываются деструкторы. Однако, если при выполнении оператора return раскрутка стека прекратиться в точке где была вызвана завершенная функция, то при при выполнении throw объекты из стека будут уничтожаться до тех пор, пока управление не будет передано в блок try<> , содержащий обработчик, соответствующий типу выброшенного исключения. Читать подробнее про обработку исключений [1].

Исключения в деструкторе класса

В связи с этим, в большинстве случаев разрушение объектов созданных на стеке (без использования new/malloc ) произойдет корректно — вызовом деструктора. Однако исключения в конструкторе или деструкторе могут приводить к нежелательным последствиям.

Во-первых, программа не должна вырабатывать исключения во время обработки другого исключения (когда происходит раскрутка стека) — это приведет к аварийному завершению работы программы (фактически вызову abort() ), которое уже не получится корректно обработать. Причина такой ошибки заключается в том, что один из деструкторов вырабатывает исключение или не обрабатывает исключение функции, которую вызывает:

destructor_exception_abort

Тем более очевидно, что если деструктор завершает работу исключением, то может возникать утечка памяти — в памяти могут остаться как части текущего класса, так и базовых классов. Из этого ясно, что деструктор никогда не должен вырабатывать исключения, а также обрабатывать все возможные исключения функций, которые вызывает — они могут приводить как к утечкам, так и к очень трудноуловимым ошибкам.

Еще один из аспектов работы деструкторов и исключений иллюстрирует следующий фрагмент кода:

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

Стандартная библиотека предоставляет функцию std::uncaught_exception , которая в деструкторе позволяет узнать, почему уничтожается объект, из-за выброшенного исключения, или же по какой-либо другой причине.

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

Исключения в конструкторе класса

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

Стандарт языка С++ гарантирует, что если исключение возникнет в конструкторе, то памяти из под членов-данных класса будет освобождена корректно вызовом деструктора — т.е. если вы используете идиому RAII [2], то проблем не будет. Часто для этого достаточно использовать std::vector/std::string вместо старых массивов и строк, и умные указатели вместо обычных [3]. Если же вы продолжите использовать сырые указатели и динамически выделять память — нужно будет очень тщательно следить за ней, например в следующем фрагменте кода нет утечки, т.к. исключение будет выработано только если память не будет выделена [4]:

Исключения позволяют обозначить, что во время выполнения программы произошла ошибка. Создаются объекты исключений, описывающие ошибку, а затем создаются с ключевым словом throw . Далее среда выполнения ищет наиболее совместимый обработчик исключений.

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

Метод не способен выполнить свои заданные функции. Например, если значение параметра метода является недопустимым:

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

Когда аргумент метода вызывает исключение. В этом случае должно быть перехвачено исходное исключение и создан экземпляр ArgumentException. Исходное исключение должно передаваться конструктору ArgumentException в качестве параметра InnerException:

[! ПРИМЕЧАНИЕ]Приведенный выше пример предназначен для иллюстрационных целей. Проверка индекса с помощью исключений в большинстве случаев является плохой практикой. Исключения должны быть зарезервированы для защиты от исключительных условий программы, а не для проверки аргументов, как описано выше.

Исключения содержат свойство с именем StackTrace. Строка содержит имена методов в текущем стеке вызовов вместе с именем файла и номером строки, в которой было вызвано исключение для каждого метода. Объект StackTrace создается автоматически средой CLR из точки оператора throw , так что исключения должны вызываться из той точки, где должна начинаться трассировка стека.

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

Чего следует избегать при вызове исключений

Ниже приводятся рекомендации по тому, чего следует избегать при вызове исключений.

Определение классов исключений

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

This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

  • Open with Desktop
  • View raw
  • Copy raw contents Copy raw contents

Copy raw contents

Copy raw contents

Возврат кода ошибки

- Ошибку можно проигнорировать

- Делает код громозким

Поддержка со стороны C++

- При неправильном использовании могут усложнить программу

Что такое исключительная ситуация?

Ошибка которую нельзя обработать на данном уровне и игнорирование которой делает дальнейшую работу программы бессмысленной.

Гарантии безопасности исключений (exception safety)

  1. Гарантировано искючений нет (No-throw guarantee)

Операции всегда завершаются успешно, если исключительная ситуация возникла она обрабатывается внутри операции.

Также известна как коммит ролбек семантика (commit/rollback semantics). Операции могут завершиться неудачей, но неудачные операции гарантированно не имеют побочных эффектов, поэтому все данные сохраняют свои исходные значения.

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

Поиск подходящего обработчика

  1. Поиск подходящего обработчика идет в порядке следования обработчиков в коде
  2. Полного соответствия типа не требуется, будет выбран первый подходящий обработчик
  3. Если перехватывать исключение по значению, то возможна срезка до базового класса
  4. Если наиболее общий обработчик идет раньше, то более специализированный обработчик никогда не будет вызван
  5. Три точки - перехват любого исключения

Исключения ОС - не исключения С++, например, деление на ноль. Для их обработки нужно использовать средства предоставляемые конкретной платформой

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

Если подходящий обработчик не был найден вызывается стандартная функция terminate.

Вызывает стандартную функцию С - abort.

abort - аварийное завершение программы, деструкторы объектов вызваны не будут.

Поведение terimnate можно изменить установив свой обработчик функцией set_terminate.

Где уместен catch (. )?

Только в main, для того, чтобы поймать необработанное исключение, чтобы избежать вызов terminate и таким образом завершить работу с вызовом деструкторов.

noexcept говорит компилятору, что функция не выбрасывает исключений - это позволяет компилятору генерировать более компактный код, но если фактически исключение было выброшено, то будет вызвана функция terminate.

Исключения в деструкторе

Исключение покинувшее деструктор во время раскрутки стека или у глобального/статического объекта приведет к вызову terminate.

Начиная с С++11 все деструкторы компилятором воспринимаются как помеченные noexcept - теперь исключения не должны покидать деструктора никогда.

Исключения в конструкторе

Клиент либо получает объект в консистентном состоянии, либо не получает ничего.

Для полностью сконструированных на момент выброса исключения объектов будут вызваны деструкторы, память выделенная под объект будет корректно освобождена, но поскольку объект не был полностью сконструирован, то деструктор вызван не будет.

Читайте также: