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

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

47 голосов
/ 01 октября 2008

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

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

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

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

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

Приложение

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

Ответы [ 7 ]

18 голосов
/ 01 октября 2008

Вы НЕ ДОЛЖНЫ выбросить исключение из деструктора.

Примечание: обновлено для ссылки на изменения в стандарте:

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

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

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

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

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

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

исключение из деструктора

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

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

8 голосов
/ 02 октября 2008

Из оригинального вопроса:

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

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

  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!
  }
};

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

Edit-Add

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

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"
      }
    }
  }
}

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

4 голосов
/ 17 октября 2008

Это напоминает мне вопрос от коллеги, когда я объяснил ему концепцию исключения / RAII: «Эй, какое исключение я могу выбросить, если компьютер выключен?»

В любом случае, я согласен с ответом Мартина Йорка RAII против исключений

Как обстоят дела с исключениями и деструкторами?

Многие функции C ++ зависят от деструкторов, не производящих метание.

Фактически, вся концепция RAII и ее взаимодействие с ветвлением кода (возвраты, выбросы и т. Д.) Основаны на том факте, что освобождение не приведет к ошибке. Точно так же некоторые функции не должны завершаться сбоем (например, std :: swap), когда вы хотите предложить своим объектам гарантии высокого исключения.

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

Что произойдет, если это будет разрешено?

Просто ради забавы, я пытался это представить ...

Если ваш деструктор не освободит ваш ресурс, что вы будете делать? Ваш объект, вероятно, наполовину разрушен, что бы вы сделали из "внешнего" улова с этой информацией? Попробуйте снова? (если да, то почему бы не попытаться снова изнутри деструктора? ...)

То есть, если бы вы могли в любом случае получить доступ к своему полуразрушенному объекту: что, если ваш объект находится в стеке (что является основным способом работы RAII)? Как вы можете получить доступ к объекту за его пределами?

Отправка ресурса внутри исключения?

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

А теперь представьте что-нибудь смешное:

 void doSomething()
 {
    try
    {
       MyResource A, B, C, D, E ;

       // do something with A, B, C, D and E

       // Now we quit the scope...
       // destruction of E, then D, then C, then B and then A
    }
    catch(const MyResourceException & e)
    {
       // Do something with the exception...
    }
 }

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

Но ...

Отправка ресурсов MULTIPLE внутри исключений MULTIPLE?

Теперь, если ~ D может потерпеть неудачу, то ~ C тоже может. а также ~ B и ~ A.

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

    catch(const std::vector<MyResourceException> & e)
    {
       // Do something with the vector of exceptions...
       // Let's hope if was not caused by an out-of-memory problem
    }

Давайте вернемся ( Мне нравится эта музыка ... ): Каждое выбрасываемое исключение отличается (, потому что причина другая: Помните, что в C ++ исключения не должны происходить из std :: исключение ). Теперь вам нужно одновременно обработать четыре исключения. Как вы могли бы написать предложения catch, обрабатывающие четыре исключения по их типам и по порядку их выдачи?

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

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

Очевидно, что разработчики C ++ не нашли жизнеспособного решения, а просто сократили свои потери там.

Проблема не в RAII, а в исключениях ...

Нет, проблема в том, что иногда вещи могут потерпеть неудачу настолько, что ничего не поделаешь.

RAII хорошо работает с исключениями, если выполняются некоторые условия. Среди них: Деструкторы не будут выбрасывать . То, что вы видите как оппозицию, - это всего лишь угловой случай единого шаблона, объединяющего два «имени»: исключение и RAII

В случае возникновения проблемы в деструкторе, мы должны принять поражение и спасти то, что можно спасти : «Не удалось освободить соединение с БД? Извините. По крайней мере, давайте избежим этой утечки памяти и закройте этот файл. "

Хотя шаблон исключения является (предполагается) основной обработкой ошибок в C ++, он не единственный. Вы должны обрабатывать исключительные (каламбур) случаи, когда исключения C ++ не являются решением, используя другие механизмы ошибок / журналов.

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

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

Лучшее, что вы можете сделать, вы уже написали это. Мои собственные предпочтения связаны с методом метания finalize, ресурсами очистки деструктора non-throwing, которые не были завершены вручную, и журналом / окном сообщений (если возможно) для предупреждения о сбое в деструкторе.

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

: -)

3 голосов
/ 09 июля 2011

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

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

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

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

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

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

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

2 голосов
/ 05 октября 2008

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

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

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

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

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

1 голос
/ 05 октября 2008

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

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

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

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

0 голосов
/ 01 октября 2008

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

bool std::uncaught_exception()

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

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

...