RAII против исключений

Чем больше мы используем RAII в C++, тем чаще мы сталкиваемся с деструкторами, которые выполняют нетривиальное освобождение. Теперь освобождение (завершение, как бы вы его ни называли) может завершиться ошибкой, и в этом случае исключения - действительно единственный способ сообщить кому-либо наверху о нашей проблеме освобождения. Но опять же, бросающие деструкторы - плохая идея из-за возможности возникновения исключений во время раскрутки стека. std::uncaught_exception() позволяет вам узнать, когда это произойдет, но не более того, поэтому, помимо разрешения вам регистрировать сообщение перед завершением, вы мало что можете сделать, если только вы не хотите оставить свою программу в неопределенном состоянии, когда некоторые вещи освобождены / доработаны и некоторые нет.

Один из подходов - не использовать деструкторы. Но во многих случаях это просто скрывает настоящую ошибку. Наш деструктор может, например, закрывать некоторые управляемые RAII соединения с БД в результате возникновения какого-либо исключения, и эти соединения с БД могут не закрыться. Это не обязательно означает, что мы в порядке с завершением программы на этом этапе. С другой стороны, регистрация и отслеживание этих ошибок не является решением для каждого случая; в противном случае у нас не было бы нужды в исключениях для начала. С деструкторами без броска нам также приходится создавать функции «reset ()», которые должны вызываться перед уничтожением, но это просто сводит на нет всю цель RAII.

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

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

Так что либо RAII, либо исключения. Не так ли? Я склоняюсь к деструкторам без броска; главным образом потому, что он упрощает работу (r). Но я действительно надеюсь, что есть лучшее решение, потому что, как я уже сказал, чем больше мы используем RAII, тем больше мы используем dtors, которые делают нетривиальные вещи.

Приложение

Я добавляю ссылки на интересные статьи и обсуждения по теме, которые я нашел:

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

Andy 20.02.2018 11:34
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
49
1
7 889
6

Ответы 6

Вы НЕ СЛЕДУЕТ выбрасываете исключение из деструктора.

Примечание. Обновлено, чтобы отразить изменения в стандарте:

В C++ 03
Если исключение уже распространяется, приложение будет остановлено.

В C++ 11
Если деструктор - noexcept (по умолчанию), приложение будет завершено.

Следующее основано на C++ 11

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

Следующее основано на C++ 03

Под прекращением я подразумеваю немедленную остановку Разматывание стопки прекращается. Деструкторы больше не вызываются. Все плохое. См. Обсуждение здесь.

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

Я не слежу (и не согласен с) вашей логикой, что это усложняет деструктор. При правильном использовании интеллектуальных указателей это фактически упрощает деструктор, поскольку теперь все становится автоматическим. Каждый класс собирает свой кусочек головоломки. Никакой операции на головном мозге или ракетостроения здесь нет. Еще одна большая победа для RAII.

Что касается возможности std :: uncaught_exception (), я указываю вам на Статья Херба Саттерса о том, почему это не работает

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

Assaf Lavie 02.10.2008 08:10

Я согласен с Ассафом. Вы помещаете больше (очистки) материала в dtors, чтобы он выполнялся при возникновении исключений, но вырожденный случай этого - функция только с ctors и (неявными) dtors. Так что в dtors много всего -> есть возможность для множества исключений.

QBziZ 02.10.2008 13:20

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

Martin York 02.10.2008 20:58

@QBziZ Я согласен с Мартином: то, что вы говорите, очень похоже на «чем больше кода мы добавляем в программу, тем больше мы рискуем, что-то потерпит неудачу». Как и любая функция очистки, деструктор должен ПЫТАТЬСЯ на очистку. Но в отличие от других функций, он не может выбросить, потому что деструктор НЕ похож на другие функции.

paercebal 17.10.2008 02:27

Вы можете определить, есть ли в настоящее время исключение в полете (например, мы находимся между блоками throw и catch, выполняя раскручивание стека, возможно, копируя объекты исключения или подобное), проверив

bool std::uncaught_exception()

Если он возвращает true, бросок в этот момент завершит программу. Если нет, бросок безопасен (или, по крайней мере, так же безопасен, как и когда-либо). Это обсуждается в разделах 15.2 и 15.5.3 ISO 14882 (стандарт C++).

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

ПРИМЕЧАНИЕ: см. Сообщение Мартина выше ... это может вернуть истину даже при попытке {..}, если активна очистка исключения.

Aaron 02.10.2008 12:04

Из исходного вопроса:

Now, deallocation (finalization, however you want to call it) can fail, in which case exceptions are really the only way to let anybody upstairs know of our deallocation problem

Отсутствие очистки ресурса указывает либо на:

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

  2. Ошибка распределителя или недоработка в конструкции. Обратитесь к документации. Скорее всего, ошибка существует, чтобы помочь диагностировать ошибки программиста. См. Пункт 1 выше.

  3. В противном случае неустранимое неблагоприятное состояние, которое может быть продолжено.

Например, в бесплатном магазине C++ есть безотказный оператор delete. Другие API (например, Win32) предоставляют коды ошибок, но не работают только из-за ошибки программиста или аппаратного сбоя, с ошибками, указывающими на такие условия, как повреждение кучи, двойное освобождение и т. д.

Что касается неустранимых неблагоприятных условий, возьмем соединение с БД. Если закрыть соединение не удалось, потому что соединение было разорвано - круто, готово. Не бросай! Разрыв соединения (должен) привести к закрытому соединению, поэтому больше ничего делать не нужно. Во всяком случае, запишите сообщение трассировки, чтобы помочь диагностировать проблемы использования. Пример:

class DBCon{
public:
  DBCon() { 
    handle = fooOpenDBConnection();
  }
  ~DBCon() {
    int err = fooCloseDBConnection();
    if (err){
      if (err == E_fooConnectionDropped){
        // do nothing.  must have timed out
      } else if (fooIsCriticalError(err)){
        // critical errors aren't recoverable.  log, save 
        //  restart information, and die
        std::clog << "critical DB error: " << err << "\n";
        save_recovery_information();
        std::terminate();
      } else {
        // log, in case we need to gather this info in the future,
        //  but continue normally.
        std::clog << "non-critical DB error: " << err << "\n";
      }
    }
    // done!
  }
};

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

Изменить-Добавить

Если вы В самом деле хотите сохранить какую-то связь с теми соединениями с БД, которые не могут быть закрыты - возможно, им не удалось закрыть из-за неустойчивых условий, и вы хотите повторить попытку позже - тогда вы всегда можете отложить очистку :

vector<DBHandle> to_be_closed_later;  // startup reserves space

DBCon::~DBCon(){
  int err = fooCloseDBConnection();
  if (err){
    ..
    else if ( fooIsRetryableError(err) ){
      try{
        to_be_closed.push_back(handle);
      } catch (const bad_alloc&){
        std::clog << "could not close connection, err " << err << "\n"
      }
    }
  }
}

Очень некрасиво, но это может сделать работу за вас.

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

Martin York 02.10.2008 04:02

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

Assaf Lavie 02.10.2008 08:28

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

Martin York 02.10.2008 09:33

@Assaf - обработка исключений не поддерживает такого рода восстановление. Рассмотрим случай, когда два объекта БД раскручиваются, и оба «не справляются» с очисткой. Если приложению необходимо знать о таких сбоях, оно должно вызвать для объекта член close () перед уничтожением.

Aaron 02.10.2008 11:58

@Martin - согласен, увольнение - не единственный ответ и не типичный ответ. «ничего не делать» или «изменить дизайн» - вот частые ответы. Дайте объекту метательный член close (), если вам нужно знать об ошибке. dtors должны убираться и молчать.

Aaron 02.10.2008 12:00

Есть много ситуаций, в которых правильное поведение после сбоя очистки не является ни «продолжать, как будто ничего не случилось», ни «мгновенно убить всю систему». Например, метод «сохранить документ» должен вызывать исключение, если все не работает нормально включая закрытие. Если для сохранения документа требуется запись двух файлов одновременно, а USB-накопитель отключен во время метода SaveDocument, и запись в один файл вызывает исключение (вероятно); деструктор другого файла также не сработает. Было бы очень грубо, если бы приложение немедленно прекратило работу, но ...

supercat 01.12.2012 02:50

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

supercat 01.12.2012 02:53

@supercat - к сожалению, вы не можете этого сделать. Существующее исключение может уже быть разворачивающимся, и исключение, оставляющее деструктор, сталкивающееся с существующим исключением, приводит к принудительному вызову std :: terminate (). В лучшем случае вы можете добавить функцию для проверки ошибок и дать пользователям указание вызывать ее «если они хотят» до того, как произойдет автоматическое уничтожение.

Aaron 05.02.2013 13:14

@Aaron: Жаль, что ни C#, ни Java, ни .net не предоставляют удобный метод для метода очистки, чтобы узнать, вызывается ли он при «нормальных» обстоятельствах или из-за раскрутки исключения. Возможно, лучше всего для процедуры очистки иметь какой-либо метод, который она может вызывать, чтобы сообщить, что что-то не так, и пользовательский интерфейс должен сообщать об этом кому-то, но это не совсем элегантно.

supercat 05.02.2013 18:10

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

Mooing Duck 31.01.2015 02:34

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

Похоже, вы исключаете «простую регистрацию» и не склонны прекращать работу, так что, по вашему мнению, лучше всего сделать?

Думаю, если бы у нас был ответ на этот вопрос, мы бы лучше понимали, как действовать дальше.

Мне никакая стратегия не кажется особенно очевидной; кроме всего прочего, я действительно не знаю, что означает закрытие соединения с базой данных для выброса. В каком состоянии будет соединение, если close () выдает ошибку? Он закрыт, все еще открыт или неопределенный? И если это не определено, есть ли способ вернуться к известному состоянию программы?

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

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

supercat 24.04.2014 23:06

Каковы причины, по которым ваше разрушение может потерпеть неудачу? Почему бы не позаботиться о том, чтобы обработать их, прежде чем фактически разрушить?

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

  • Выполняется транзакция. (Отметьте std :: uncaught_exception () - если true, откат, иначе фиксация - это наиболее вероятные желаемые действия, если у вас нет политики, которая гласит иначе, перед фактическим закрытием соединения.)
  • Соединение разорвано. (Обнаруживать и игнорировать. Сервер откатится автоматически.)
  • Другая ошибка БД. (Зарегистрируйте это, чтобы мы могли изучить и, возможно, обработать должным образом в будущем. Что может быть для обнаружения и игнорирования. А пока попробуйте выполнить откат и снова отключиться, игнорируя все ошибки.)

Если я правильно понимаю RAII (чего я, возможно, не знаю), все дело в его объеме. Так что это не значит, что вы все равно ХОТИТЕ, что транзакции длятся дольше, чем объект. Мне кажется разумным, что вы хотите обеспечить закрытие как можно лучше. RAII не делает это уникальным - даже без объектов (скажем, в C) вы все равно попытаетесь уловить все условия ошибок и справиться с ними как можно лучше (иногда их игнорируют). Все, что делает RAII, - это заставляет вас поместить весь этот код в одно место, независимо от того, сколько функций используют этот тип ресурса.

Вы смотрите на две вещи:

  1. RAII, который гарантирует очистку ресурсов при выходе из области видимости.
  2. Завершение операции и выяснение, удалась она или нет.

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

Исключения - это один из способов сообщить, что что-то не удалось, но, как вы говорите, существует ограничение языка C++, которое означает, что они не подходят для этого с помощью деструктора [*]. Возвращаемые значения - это еще один способ, но еще более очевидно, что деструкторы тоже не могут их использовать.

Итак, если вы хотите знать, были ли ваши данные записаны на диск, вы не можете использовать RAII для этого. Это не «уничтожает всю цель RAII», поскольку RAII все равно будет пытаться его записать, и он все равно освободит ресурсы, связанные с дескриптором файла (транзакция БД, что угодно). Он ограничивает возможности RAII - он не скажет вам, были ли данные записаны или нет, поэтому для этого вам нужна функция close(), которая может возвращать значение и / или генерировать исключение.

[*] Это тоже вполне естественное ограничение, присутствующее в других языках. Если вы думаете, что деструкторы RAII должны генерировать исключения, чтобы сказать «что-то пошло не так!», Тогда что-нибудь должен произойти, когда уже есть исключение в полете, то есть «что-то еще пошло не так даже до этого!». Я знаю, что языки, использующие исключения, не допускают одновременного выполнения двух исключений - язык и синтаксис просто не позволяют этого. Если RAII должен делать то, что вы хотите, тогда необходимо переопределить сами исключения, чтобы было разумно, чтобы в одном потоке было несколько ошибок одновременно, а два исключения распространялись наружу и вызывались два обработчика, по одному для обработки каждого.

Другие языки позволяют второму исключению скрывать первое, например, если в Java возникает блок finally. C++ в значительной степени говорит, что второй должен быть подавлен, иначе вызывается terminate (в некотором смысле подавляя оба). Ни в том, ни в другом случае более высокие уровни стека не информируются об обеих неисправностях. Что немного прискорбно, так это то, что в C++ вы не можете достоверно сказать, является ли еще одно исключение слишком большим (uncaught_exception не говорит вам об этом, он говорит вам что-то другое), поэтому вы даже не можете выбросить случай, когда там не уже исключение в полете. Но даже если бы вы могли сделать это в этом случае, вы все равно были бы напичканы тем, что еще один - это слишком много.

Другие вопросы по теме