Является ли `catch (...) {throw; } плохая практика?

74

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

try
{
  // Stuff
}
catch (...)
{
  // Some cleanup
  throw;
}

Приемлемо в тех случаях, когда RAII не применяется . (Пожалуйста, не спрашивайте ... не всем в моей компании нравится объектно-ориентированное программирование, а RAII часто рассматривается как "бесполезная школа" ...)

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

try
{
  // Stuff
}
catch (exception_type1&)
{
  // Some cleanup
  throw;
}
catch (exception_type2&)
{
  // Some cleanup
  throw;
}
catch (exception_type3&)
{
  // Some cleanup
  throw;
}

Есть ли хорошая практика в этих ситуациях?

ereOn
источник
3
@Pubby: Не уверен, что это точно такой же вопрос. Связанный вопрос больше о «Должен ли я поймать ...» , а мой вопрос сосредоточиться на «Должен ли я лучше поймать ...или <specific exception>перед тем Повторное выбрасывание»
ereOn
53
Извините, но C ++ без RAII не является C ++.
fredoverflow
46
Таким образом, ваши коровники отвергают метод, который был изобретен для решения определенной проблемы, а затем обсуждают, какую из низших альтернатив следует использовать? Извините, но это кажется глупым , независимо от того, как я на это смотрю.
ВОО
11
«ловить ... без повторного броска действительно неправильно» - вы ошибаетесь. В main, catch(...) { return EXIT_FAILURE; }может также быть прямо в коде , который не работает под отладчиком. Если вы не поймаете, то стек не может быть размотан. Только когда ваш отладчик обнаруживает неперехваченные исключения, вы хотите, чтобы они ушли main.
Стив Джессоп
3
... так что даже если это «ошибка программирования», это не обязательно означает, что вы не хотите знать об этом. В любом случае, ваши коллеги не являются хорошими профессионалами в области программного обеспечения, поэтому, как говорит sbi, очень трудно говорить о том, как лучше всего справиться с ситуацией, которая хронически слабая для начала.
Стив Джессоп

Ответы:

196

Мои коллеги говорят, что вы всегда должны знать, какие исключения должны быть брошены [...]

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

Как в мире могут такие классы, как std::vectorдаже притворяться, знать, что выбросят конструкторы копий, при этом гарантируя безопасность исключений?

Если бы вы всегда знали, что будет делать вызываемый объект во время компиляции, тогда полиморфизм будет бесполезен! Иногда вся цель состоит в том, чтобы абстрагироваться от того, что происходит на более низком уровне, поэтому вы специально не хотите знать, что происходит!

Mehrdad
источник
32
На самом деле, даже если они знали, что исключения должны быть выброшены. Какова цель этого дублирования кода? Если обработка не отличается, я не вижу смысла перечислять исключения, чтобы показать ваши знания.
Майкл Крелин - хакер
3
@ MichaelKrelin-хакер: это тоже. Кроме того, добавьте к этому тот факт, что они отказались от спецификаций исключений, потому что перечисление всех возможных исключений в коде, как правило, приводило к ошибкам позже ... это худшая идея в истории.
Мердад
4
И что меня беспокоит, так это то, что могло послужить источником такого отношения, если бы мы увидели полезную и удобную технику как «бесполезные школьные вещи». Но хорошо ...
Майкл Крелин - хакер
1
+1, перечисление всех возможных вариантов - отличный рецепт для будущей неудачи, с какой стати кто-то решил сделать это снова ...?
littleadv
2
Хороший ответ. Возможно, было бы полезно упомянуть, что если компилятор, который нужно поддерживать, имеет ошибку в области X, то использование функциональности из области X не является разумным, по крайней мере, не использовать его напрямую. Например, учитывая информацию о компании, я не был бы удивлен, если бы они использовали Visual C ++ 6.0, в котором были некоторые глупые ошибки в этой области (например, вызываемые дважды деструкторы объекта исключения) - некоторые более мелкие потомки этих ранних ошибок выжили до этот день, но требуют тщательных мер, чтобы проявить.
Альф П. Штайнбах
44

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

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

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

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

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

Так что я бы согласился, что catch(...)это в основном зло ... временно .

Это положение является надлежащим использованием RAII. Потому что без этого вы должны быть в состоянии сделать определенную очистку. Там нет обойти это; Вы должны быть в состоянии сделать уборку. Вы должны быть в состоянии гарантировать, что бросок исключения оставит код в разумном состоянии. И catch(...)это жизненно важный инструмент для этого.

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

Конечно, есть одно действительное, хотя и редкое использование, catch(...)которое даже RAII не может изгнать: получение exception_ptrперенаправления кому-то еще. Обычно через promise/futureаналогичный интерфейс.

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

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

Вкратце: это проблема, которую RAII был создан для решения (не то, что он не решает другие проблемы).

Что меня смущает в этом представлении, так это то, что большинство людей считают, что RAII - это плохо. Как правило, аргумент звучит так: «RAII - это плохо, потому что вы должны использовать исключения, чтобы сигнализировать о сбое конструктора. Но вы не можете генерировать исключения, потому что это небезопасно, и вам придется иметь много catchоператоров, чтобы все очистить». Что является ошибочным аргументом, потому что RAII решает проблему, которую создает отсутствие RAII.

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

Николь Болас
источник
2
Кажется, вы не пишете код, который обеспечивает надежную гарантию безопасности исключений. RAII помогает с предоставлением базовой гарантии. Но чтобы обеспечить надежную гарантию, вы должны отменить некоторые действия, чтобы вернуть систему в состояние, в котором она находилась до вызова функции. Основная гарантия - чистка , сильная гарантия - откат . Выполнение отката зависит от конкретной функции. Таким образом, вы не можете поместить это в "RAII". И тогда блок « лови все» становится удобным. Если вы пишете код с сильной гарантией, вы используете всеохватывающие много.
anton_rh
@anton_rh: Возможно, но даже в этих случаях всеохватывающие операторы являются инструментом последней инстанции . Предпочтительный инструмент - делать все, что выбрасывает, прежде чем изменять любое состояние, которое вам придется вернуть при исключении. Очевидно, что вы не можете реализовать все таким образом во всех случаях, но это идеальный способ получить надежную гарантию исключения.
Никол Болас
14

Два комментария, правда. Во-первых, в идеальном мире вы всегда должны знать, какие исключения могут быть созданы, на практике, если вы имеете дело со сторонними библиотеками или компилируете с помощью компилятора Microsoft, это не так. Более конкретно, однако; даже если вы точно знаете все возможные исключения, уместно ли это здесь? catch (...)выражает намерение гораздо лучше, чем catch ( std::exception const& ), даже если предположить, что все возможные исключения происходят из std::exception(что было бы в идеальном мире). Что касается использования нескольких блоков улова, если нет единой базы для всех исключений: это просто запутывание и кошмар обслуживания. Как вы узнаете, что все виды поведения идентичны? И что это было намерение? А что произойдет, если вам придется изменить поведение (например, исправление ошибок)? Это слишком легко пропустить один.


источник
3
На самом деле, мой коллега разработал свой собственный класс исключений, который не является производным std::exceptionи пытается каждый день обеспечивать его использование в нашей кодовой базе. Я предполагаю, что он пытается наказать меня за использование кода и внешних библиотек, которые он сам не написал.
ereOn
17
@ereOn Мне кажется, что твой сотрудник остро нуждается в обучении. В любом случае, я бы, вероятно, не использовал написанные им библиотеки.
2
Шаблоны и знание, какие исключения будут выброшены, идут вместе, как арахисовое масло и мертвые гекконы. Что-то такое простое, что std::vector<>может вызвать любое исключение по любой причине.
Дэвид Торнли
3
Скажите, пожалуйста, как именно вы узнаете, какое исключение будет выдано завтрашним исправлением ошибки в дереве вызовов?
Mattnz
11

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

Это означает:

try
{
  // Stuff
}
catch (...)
{
  // General stuff
}

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

Тем не мение:

try
{
  // Stuff
}
catch (exception_type_we_can_handle&)
{
  // Deal with the known exception
}

Хорошо - мы знаем, с чем имеем дело, и нам не нужно показывать это вызывающему коду.

Точно так же:

try
{
  // Stuff
}
catch (...)
{
  // Rollback transactions, log errors, etc
  throw;
}

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

Кит
источник
9

Любой ответ « да» или « нет» должен сопровождаться обоснованием того, почему это так.

Сказать, что это неправильно, просто потому, что меня так учили, - это просто слепой фанатизм.

Писать то же самое //Some cleanup; throwнесколько раз, как в вашем примере, неправильно, потому что это дублирование кода, а это бремя обслуживания. Написание всего один раз лучше.

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

Но если вы перебрасываете после a catch(...), то последнее обоснование больше не применяется, так как вы на самом деле не обрабатываете исключение, поэтому нет никаких причин, по которым это не рекомендуется.

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

void DoSomethingImportant()
{
    try
    {
        Log("Going to do something important");
        DoIt();
    }
    catch (std::exception &e)
    {
        Log("Error doing something important: %s", e.what());
        throw;
    }
    catch (...)
    {
        Log("Unexpected error doing something important");
        throw;
    }
    Log("Success doing something important");
}
pestophagous
источник
2
Будем надеяться, Log(...)не можем бросить.
Дедупликатор
2

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

Но так как все говорят это, я расскажу о том факте, что, хотя я использую их экономно, я часто смотрел на одно из своих утверждений «catch (Exception e)» и говорил: «Черт, я хотел бы позвонить Выясните конкретные исключения того времени ", потому что, когда вы приходите позже, часто приятно знать, каково было намерение и что клиент может бросить с первого взгляда.

Я не оправдываю позицию «Всегда используйте x», просто говорю, что иногда приятно видеть их в списке, и я уверен, что именно поэтому некоторые люди считают, что это «правильный» путь.

Билл К
источник