В isocpp.org исключения FAQ государства
Не используйте throw, чтобы указать на ошибку кодирования при использовании функции. Используйте assert или другой механизм для отправки процесса в отладчик или для сбоя процесса и сбора аварийного дампа для отладки разработчиком.
С другой стороны, стандартная библиотека определяет std :: logic_error и все его производные, которые, как мне кажется, должны обрабатывать, помимо прочего, ошибки программирования. Передача пустой строки в std :: stof (сгенерирует invalid_argument) не является ошибкой программирования? Передача строки, содержащей символы, отличные от '1' / '0', в std :: bitset (сгенерирует invalid_argument) не является ошибкой программирования? Вызов std :: bitset :: set с недопустимым индексом (выдает out_of_range) не является ошибкой программирования? Если это не так, то в чем заключается ошибка программирования, которую можно было бы проверить? Конструктор на основе строк std :: bitset существует только начиная с C ++ 11, поэтому его следует разрабатывать с идиоматическим использованием исключений. С другой стороны, мне говорили, что logic_error вообще не должен использоваться.
Другое правило, которое часто встречается с исключениями: «Используйте исключения только в исключительных случаях». Но как библиотечная функция должна знать, какие обстоятельства являются исключительными? Для некоторых программ невозможность открыть файл может быть исключительной. Для других неспособность выделить память не может быть исключительной. И есть сотни случаев между ними. Будучи не в состоянии создать сокет? Невозможно подключиться или записать данные в сокет или файл? Не удалось разобрать ввод? Может быть исключительным, может и не быть. Сама функция определенно не может вообще знать, она не знает, в каком контексте она вызывается.
Итак, как я должен решить, следует ли мне использовать исключения или нет для конкретной функции? Мне кажется, что единственный действительно непротиворечивый способ - это использовать их для обработки всех без исключения ошибок или ни для чего. И если я использую стандартную библиотеку, этот выбор был сделан для меня.
источник
Ответы:
Во-первых, я чувствую себя обязанным указать, что
std::exception
и его дети были разработаны давно. Есть ряд деталей, которые, вероятно, (почти наверняка) будут другими, если бы они разрабатывались сегодня.Не поймите меня неправильно: есть части дизайна, которые сработали довольно хорошо, и являются довольно хорошими примерами того, как спроектировать иерархию исключений для C ++ (например, тот факт, что, в отличие от большинства других классов, все они разделяют общий корень).
Если смотреть конкретно
logic_error
, у нас есть немного загадки. С одной стороны, если у вас есть какой-либо разумный выбор в этом вопросе, совет, который вы цитировали, верен: как правило, лучше всего терпеть неудачу настолько быстро и шумно, насколько это возможно, чтобы его можно было отлаживать и исправлять.Однако, к лучшему или худшему, сложно определить стандартную библиотеку вокруг того, что вы обычно должны делать. Если он определил их для выхода из программы (например, вызова
abort()
) при неправильном вводе, это было бы то, что всегда происходило для этого обстоятельства - и на самом деле существует довольно много обстоятельств, при которых это, вероятно, не совсем правильно делать по крайней мере в развернутом коде.Это будет применяться в коде с (по крайней мере мягкими) требованиями в реальном времени и минимальным штрафом за неправильный вывод. Например, рассмотрим программу чата. Если он декодирует некоторые голосовые данные и получает неправильный ввод, скорее всего, пользователь будет гораздо счастливее жить с миллисекундами статического сигнала на выходе, чем программа, которая полностью отключается. Аналогично, при воспроизведении видео может быть более приемлемым жить с созданием неправильных значений для некоторых пикселей для одного или двух кадров, чем если программа в итоге выйдет из-за повреждения входного потока.
Что касается использования исключений для сообщения об определенных типах ошибок: вы правы - одна и та же операция может квалифицироваться как исключение или нет, в зависимости от того, как она используется.
С другой стороны, вы также ошибаетесь - использование стандартной библиотеки не обязательно навязывает вам это решение. В случае открытия файла вы обычно используете iostream. Iostreams не совсем последний и лучший дизайн, но в этом случае они делают все правильно: они позволяют вам установить режим ошибки, так что вы можете контролировать, если открытие файла не приводит к возникновению исключения или нет. Итак, если у вас есть файл, который действительно необходим для вашего приложения, и если вы не можете открыть его, это означает, что вы должны предпринять некоторые серьезные корректирующие действия, то вы можете заставить его вызвать исключение, если он не сможет открыть этот файл. Для большинства файлов, которые вы попытаетесь открыть, если они не существуют или недоступны, они просто потерпят неудачу (это по умолчанию).
Что касается того, как вы решите: я не думаю, что есть простой ответ. Хорошо это или плохо, но «исключительные обстоятельства» не всегда легко измерить. Хотя, безусловно, есть случаи, которые легко решить, они должны быть [не] исключительными, но есть (и, вероятно, всегда будут) случаи, когда это может быть под вопросом или требует знания контекста, который находится за пределами области рассматриваемой функции. Для подобных случаев, по крайней мере, может быть целесообразно рассмотреть проект, примерно похожий на эту часть iostreams, где пользователь может решить, приведет ли сбой к исключению или нет. В качестве альтернативы вполне возможно иметь два отдельных набора функций (или классов и т. Д.), Один из которых будет выдавать исключения, указывающие на сбой, а другой - использовать другие средства. Если вы идете по этому маршруту,
источник
Вы можете не верить этому, но разные C ++ кодеры не согласны. Вот почему FAQ говорит об одном, но стандартная библиотека не согласна.
FAQ защищает от сбоев, потому что это будет легче отлаживать. Если вы потерпите крах и получите дамп ядра, у вас будет точное состояние вашего приложения. Если вы выбросите исключение, вы потеряете большую часть этого состояния.
Стандартная библиотека использует теорию, согласно которой предоставление кодеру способности отлавливать и обрабатывать ошибки важнее отладки.
Идея в том, что если ваша функция не знает, является ли ситуация исключительной, она не должна выдавать исключение. Он должен вернуть состояние ошибки через какой-то другой механизм. Как только он достигает точки в программе, где он знает, что состояние является исключительным, он должен выбросить исключение.
Но это имеет свои проблемы. Если из функции возвращается состояние ошибки, вы можете не забыть проверить его, и ошибка пройдет молча. Это приводит к тому, что некоторые люди отказываются от исключений, являющихся исключительным правилом, в пользу создания исключений для любого типа состояния ошибки.
В целом, ключевой момент заключается в том, что у разных людей разные представления о том, когда создавать исключения. Вы не найдете единой идеи. Несмотря на то, что некоторые люди будут догматично утверждать, что тот или иной способ обработки исключений не существует, единой согласованной теории не существует.
Вы можете бросить исключения:
и найти кого-то в Интернете, кто согласен с вами. Вам придется принять стиль, который работает для вас.
источник
Многие другие хорошие ответы были написаны, я просто хочу добавить короткую точку.
Традиционный ответ, особенно когда был написан FAQ по ISO C ++, в основном сравнивает «исключение C ++» с «кодом возврата в стиле C». Третий вариант «вернуть некоторый тип составного значения, например,
struct
илиunion
, или в настоящее время,boost::variant
или (предложенный)std::expected
, не рассматривается.До C ++ 11 опция «возвращать составной тип» обычно была очень слабой. Потому что семантики перемещения не было, поэтому копирование объектов в структуру и из нее было потенциально очень дорогим. В то время на языке было чрезвычайно важно стилизовать ваш код в сторону RVO , чтобы добиться максимальной производительности. Исключения были похожи на простой способ эффективно вернуть составной тип, в противном случае это было бы довольно сложно.
IMO, после C ++ 11, эта опция «вернуть различенное объединение», подобная идиоме,
Result<T, E>
используемой в Rust в наши дни, должна быть предпочтительнее в коде C ++. Иногда это действительно более простой и удобный способ указания ошибок. За исключением, всегда есть такая возможность, что функции, которые раньше не генерировались, могли внезапно начать генерировать после рефакторинга, и программисты не всегда хорошо документируют такие вещи. Когда ошибка указывается как часть возвращаемого значения в различаемом объединении, это значительно снижает вероятность того, что программист просто проигнорирует код ошибки, что является обычной критикой обработки ошибок в стиле Си.Обычно
Result<T, E>
работает вроде как опционально. Вы можете проверить, используяoperator bool
, если это значение или ошибка. А затем используйте sayoperator *
для доступа к значению или какой-либо другой функции get. Обычно этот доступ не проверяется для скорости. Но вы можете сделать так, чтобы в отладочной сборке доступ становился проверенным, и утверждение гарантировало, что на самом деле есть значение, а не ошибка. Таким образом, любой, кто не проверяет ошибки должным образом, получит твердое утверждение, а не более коварную проблему.Дополнительным преимуществом является то, что в отличие от исключений, когда, если он не перехватывается, он просто поднимает стек на произвольное расстояние, с этим стилем, когда функция начинает сигнализировать об ошибке, где она не была раньше, вы не можете скомпилировать, если только Код изменен, чтобы справиться с этим. Это делает проблемы громче - традиционная проблема «неперехваченного исключения» больше напоминает ошибку времени компиляции, чем ошибку времени выполнения.
Я стал большим поклонником этого стиля. Обычно я сейчас использую либо это, либо исключения. Но я стараюсь ограничить исключения основными проблемами. Для чего-то вроде ошибки синтаксического анализа, я пытаюсь вернуться,
expected<T>
например. Такие вещи, какstd::stoi
иboost::lexical_cast
которые выдают исключение C ++ в случае некоторой относительно незначительной проблемы «строка не может быть преобразована в число», кажутся мне сегодня очень плохим вкусом.источник
std::expected
все еще не принято предложение, верно?exception_ptr
, или вы просто хотите использовать некоторый тип структуры или что-то еще? как это.[[nodiscard]] attribute
будет полезен для этого подхода к обработке ошибок, поскольку он гарантирует, что вы не просто проигнорируете результат ошибки случайно.except_ptr
) вам пришлось вызвать внутреннее исключение. Лично я считаю, что такой инструмент должен работать совершенно независимо от исключений. Просто замечание.Это очень субъективная проблема, так как она является частью дизайна. А поскольку дизайн - это в основном искусство, я предпочитаю обсуждать эти вещи, а не спорить (я не говорю, что вы спорите).
Для меня исключительные случаи бывают двух видов: те, которые имеют дело с ресурсами, и те, которые имеют дело с критическими операциями. То, что можно считать критическим, зависит от имеющейся проблемы и, во многих случаях, от точки зрения программиста.
Неспособность получить ресурсы является главным кандидатом для создания исключений. Ресурс может быть памятью, файлом, сетевым подключением или чем-то еще, в зависимости от вашей проблемы и платформы. Теперь, отказ в выпуске ресурса гарантирует исключение? Ну, это опять зависит. Я ничего не делал, когда освобождение памяти не удавалось, поэтому я не уверен в этом сценарии. Тем не менее, удаление файлов как часть выпуска ресурса может произойти сбой, и мне это не удалось, и этот сбой обычно связан с другим процессом, который оставил его открытым в многопроцессорном приложении. Я предполагаю, что другие ресурсы могут потерпеть неудачу во время выпуска, как файл, и, как правило, это ошибка проектирования, которая вызывает эту проблему, поэтому ее исправление будет лучше, чем создание исключений.
Затем идет обновление ресурсов. Этот момент, по крайней мере для меня, тесно связан с аспектом критических операций приложения. Представьте себе
Employee
класс с функцией,UpdateDetails(std::string&)
которая изменяет детали, основываясь на заданной строке через запятую. Подобно сбоям освобождения памяти, мне трудно представить, что присваивание значений переменных-членов терпит неудачу из-за отсутствия у меня опыта в таких областях, где это может произойти. ОднакоUpdateDetailsAndUpdateFile(std::string&)
ожидается , что функция, подобная которой, как следует из названия, завершится с ошибкой. Это то, что я называю критической операцией.Теперь вы должны увидеть, требует ли так называемая критическая операция исключения. Я имею в виду, происходит ли обновление файла в конце, как в деструкторе, или это просто параноидальный вызов после каждого обновления? Существует ли резервный механизм, который регулярно пишет неписанные объекты? Я хочу сказать, что вы должны оценить критичность операции.
Очевидно, что есть много критических операций, которые не привязаны к ресурсу. Если
UpdateDetails()
даны неверные данные, он не будет обновлять детали, и ошибка должна быть известна, так что вы бы выбросили здесь исключение. Но представьте себе такую функцию, какGiveRaise()
. Теперь, если упомянутому сотруднику повезло иметь заостренного босса и он не получит повышение (с точки зрения программирования, значение некоторой переменной не позволяет этому произойти), функция по существу потерпела неудачу. Вы бы бросили исключение здесь? Я хочу сказать, что вы должны оценить необходимость исключения.Для меня последовательность - это мой подход к дизайну, а не юзабилити моих занятий. Я имею в виду, я не думаю, что с точки зрения «все функции Get должны делать это, а все функции Update должны это делать», но я вижу, апеллирует ли конкретная функция к определенной идее в рамках моего подхода. На первый взгляд, классы могут выглядеть как «случайные», но всякий раз, когда пользователи (в основном коллеги из других команд) рассуждают или спрашивают об этом, я объясню, и они кажутся довольными.
Я вижу многих людей, которые в основном заменяют возвращаемые значения исключениями, потому что они используют C ++, а не C, и что это дает «хорошее разделение обработки ошибок» и т. Д., И призывают меня прекратить «смешивать» языки и т. Д. Я обычно держусь подальше от таких людей.
источник
Во- первых, как уже говорилось, все не что ясна в C ++, ИМХО в основном потому , что требования и ограничения являются несколько более варьировалась в C ++ , чем другие языки, особ. C # и Java, которые имеют "похожие" проблемы исключений.
Я покажу на примере std :: stof:
Основной контракт , на мой взгляд, этой функции заключается в том, что она пытается преобразовать свой аргумент в число с плавающей запятой, и любой отказ сделать это сообщается исключением. Оба возможных исключения являются производными,
logic_error
но не в смысле ошибки программиста, а в смысле «входные данные никогда не могут быть преобразованы в число с плавающей запятой».Здесь можно сказать, что
logic_error
используется, чтобы указать, что, учитывая, что (во время выполнения) ввод, всегда пытаться преобразовать его - ошибка, но задача функции - определить это и сообщить вам (через исключение).Примечание: в этом представлении a
runtime_error
можно рассматривать как нечто, что при одинаковом входе в функцию теоретически может быть успешным для разных прогонов. (например, файловая операция, доступ к БД и т. д.)Дополнительное примечание: библиотека регулярных выражений C ++ выбрала свою ошибку,
runtime_error
хотя бывают случаи, когда ее можно классифицировать так же, как здесь (недопустимый шаблон регулярных выражений).Это просто показывает, IMHO, что группировка
logic_
илиruntime_
ошибка довольно нечетки в C ++ и не очень помогают в общем случае (*) - если вам нужно обрабатывать конкретные ошибки, вам, вероятно, нужно отлавливать меньше, чем две.(*): Это не означает , что один кусок кода не должен быть последовательным, но будет ли вы бросить
runtime_
илиlogic_
илиcustom_
нечто действительно не так уж важно, как мне кажется.Комментировать оба
stof
иbitset
:Обе функции принимают строки в качестве аргумента, и в обоих случаях это так:
Это утверждение имеет, ИМХО, два корня:
Производительность : если функция вызывается по критическому пути, и «исключительный» случай не является исключительным, т. Е. Значительное количество проходов будет связано с созданием исключения, то платить каждый раз за механизм исключения-разматывания не имеет смысла и может быть слишком медленным.
Местность обработки ошибок : Если функция вызывается и исключение сразу пойманы и обработаны, то есть мало смысла бросать исключение, так как обработка ошибок будет более многословным с
catch
, чем сif
.Пример:
Вот где такие функции, как
TryParse
vs.,Parse
вступают в игру: одна версия, когда локальный код ожидает, что проанализированная строка является действительной, одна версия, когда локальный код предполагает, что на самом деле ожидается (то есть, не является исключительным), что синтаксический анализ завершится неудачно.На самом деле,
stof
это просто (определяется как) оболочкаstrtof
, поэтому, если вы не хотите исключений, используйте это.ИМХО, у вас есть два случая:
Функция, подобная «библиотеке» (часто используемая в разных контекстах): Вы в принципе не можете решить. Возможно, предоставьте обе версии, возможно ту, которая сообщает об ошибке, и обертку, которая преобразует возвращенную ошибку в исключение.
Функция «приложение» (специфичная для большого количества кода приложения, может быть использована повторно, но ограничена стилем обработки ошибок приложений и т. Д.): Здесь она часто должна быть достаточно четкой. Если пути кода, вызывающие функции, обрабатывают исключения разумным и полезным способом, используйте исключения, чтобы сообщить о любой (но см. Ниже) ошибке. Если код приложения легче читать и писать для стиля возврата ошибки, обязательно используйте его.
Конечно, между ними будут места - просто используйте то, что нужно, и помните YAGNI.
Наконец, я думаю, что я должен вернуться к утверждению FAQ,
Я подписываюсь на это для всех ошибок, которые являются явным признаком того, что что-то серьезно испорчено или что вызывающий код явно не знал, что он делал.
Но когда это уместно, это часто сильно зависит от приложения, поэтому смотрите выше домен библиотеки или домен приложения.
Это возвращает нас к вопросу о том, нужно ли и как проверять предварительные условия вызова , но я не буду вдаваться в подробности, отвечу уже слишком долго :-)
источник