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

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

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

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

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

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

Приложение

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

Ответ 1

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

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

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

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

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

Ответ 2

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

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

Невозможность очистки ресурса указывает:

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

  • Исключение ошибки или ошибки дизайна. Обратитесь к документации. Скорее всего, есть ошибка, чтобы помочь диагностировать ошибки программиста. См. Пункт 1 выше.

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

Например, в свободном хранилище С++ есть оператор no-fail 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

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

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

Совсем не очень, но это может сделать работу для вас.

Ответ 3

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

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

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

Многие функции С++ зависят от не-бросающих деструкторов.

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

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

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

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

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

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

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

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

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

 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 с массивом исключений (пусть надеется, что код, созданный для этого, не... er... throw).

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

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

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

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

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

Проблема не в RAII vs Exceptions...

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

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

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

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

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

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

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

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

: -)

Ответ 4

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

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

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

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

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

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

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

Ответ 5

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

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

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

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

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

Ответ 6

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

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

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

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

Ответ 7

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

bool std::uncaught_exception()

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

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