Идиоматическое использование исключений в C ++

16

В 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 вообще не должен использоваться.

Другое правило, которое часто встречается с исключениями: «Используйте исключения только в исключительных случаях». Но как библиотечная функция должна знать, какие обстоятельства являются исключительными? Для некоторых программ невозможность открыть файл может быть исключительной. Для других неспособность выделить память не может быть исключительной. И есть сотни случаев между ними. Будучи не в состоянии создать сокет? Невозможно подключиться или записать данные в сокет или файл? Не удалось разобрать ввод? Может быть исключительным, может и не быть. Сама функция определенно не может вообще знать, она не знает, в каком контексте она вызывается.

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

cooky451
источник
6
Вы должны очень внимательно прочитать эту запись FAQ . Это относится только к ошибкам кодирования, недопустимым данным, разыменованию нулевого объекта или к чему-либо, имеющему отношение к общему ухудшению во время выполнения. В общем, утверждения касаются выявления вещей, которые никогда не должны происходить. Для всего остального есть исключения, коды ошибок и пр.
Роберт Харви
1
@RobertHarvey это определение все еще имеет ту же проблему - если что-то может быть решено без вмешательства человека или нет, известно только верхним уровням программы.
cooky451
1
Вы зацикливаетесь на легализации. Оцените плюсы и минусы и решите сами. Кроме того, последний абзац в вашем вопросе ... Я не считаю это самоочевидным. Ваше мышление очень черно-белое, когда истина, вероятно, ближе к нескольким оттенкам серого.
Роберт Харви
4
Вы пытались провести какое-либо исследование, прежде чем задать этот вопрос? Идиомы обработки ошибок C ++ почти наверняка обсуждаются с отвратительными подробностями в Интернете. Одна ссылка на одну запись FAQ не является хорошим исследованием. После того, как вы проведете свое исследование, вам все равно придется определиться. Не начинайте меня с того, как наши школы программирования, по-видимому, создают бессмысленных программных роботов, которые не умеют думать самостоятельно.
Роберт Харви
2
Что подтверждает мою теорию о том, что такого правила на самом деле не может быть. Я пригласил некоторых людей из C ++ Lounge, чтобы посмотреть, смогут ли они ответить на ваш вопрос, хотя каждый раз, когда я туда захожу, их совет звучит так: «Прекратите использовать C ++, это вызовет у вас мозги». Так что примите их советы на свой страх и риск.
Роберт Харви

Ответы:

15

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

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

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

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

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

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

С другой стороны, вы также ошибаетесь - использование стандартной библиотеки не обязательно навязывает вам это решение. В случае открытия файла вы обычно используете iostream. Iostreams не совсем последний и лучший дизайн, но в этом случае они делают все правильно: они позволяют вам установить режим ошибки, так что вы можете контролировать, если открытие файла не приводит к возникновению исключения или нет. Итак, если у вас есть файл, который действительно необходим для вашего приложения, и если вы не можете открыть его, это означает, что вы должны предпринять некоторые серьезные корректирующие действия, то вы можете заставить его вызвать исключение, если он не сможет открыть этот файл. Для большинства файлов, которые вы попытаетесь открыть, если они не существуют или недоступны, они просто потерпят неудачу (это по умолчанию).

Что касается того, как вы решите: я не думаю, что есть простой ответ. Хорошо это или плохо, но «исключительные обстоятельства» не всегда легко измерить. Хотя, безусловно, есть случаи, которые легко решить, они должны быть [не] исключительными, но есть (и, вероятно, всегда будут) случаи, когда это может быть под вопросом или требует знания контекста, который находится за пределами области рассматриваемой функции. Для подобных случаев, по крайней мере, может быть целесообразно рассмотреть проект, примерно похожий на эту часть iostreams, где пользователь может решить, приведет ли сбой к исключению или нет. В качестве альтернативы вполне возможно иметь два отдельных набора функций (или классов и т. Д.), Один из которых будет выдавать исключения, указывающие на сбой, а другой - использовать другие средства. Если вы идете по этому маршруту,

Джерри Гроб
источник
9

Конструктор на основе строк std :: bitset существует только начиная с C ++ 11, поэтому его следует разрабатывать с идиоматическим использованием исключений. С другой стороны, мне говорили, что logic_error вообще не должен использоваться.

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

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

Стандартная библиотека использует теорию, согласно которой предоставление кодеру способности отлавливать и обрабатывать ошибки важнее отладки.

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

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

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

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

Вы можете бросить исключения:

  1. Никогда
  2. Где угодно
  3. Только на ошибки программиста
  4. Никогда на ошибки программиста
  5. Только во время нестандартных (исключительных) сбоев

и найти кого-то в Интернете, кто согласен с вами. Вам придется принять стиль, который работает для вас.

Уинстон Эверт
источник
Возможно, стоит отметить, что предложение использовать исключения только в тех случаях, когда обстоятельства действительно исключительные, широко пропагандируется людьми, которые преподают языки, где исключения неэффективны. C ++ не является одним из этих языков.
Жюль
1
@Jules - теперь это (производительность), безусловно, заслуживает отдельного ответа, где вы подтверждаете свои претензии. Производительность исключений в C ++ , безусловно, является проблемой, может быть больше, может быть, меньше, чем где-либо еще, но утверждение «C ++ не является одним из тех языков [где исключения имеют низкую производительность]», безусловно, является спорным.
Мартин Ба
1
@MartinBa - по сравнению, скажем, с Java, производительность исключений в C ++ на несколько порядков выше. Тесты показывают, что производительность при создании исключения на 1 уровень примерно в 50 раз медленнее, чем обработка возвращаемого значения в C ++, по сравнению с более чем в 1000 раз медленнее в Java. Рекомендации, написанные для Java в этом случае, не должны применяться к C ++ без лишних раздумий, потому что между ними разница в производительности более чем на порядок. Возможно, мне следовало написать «крайне низкая производительность», а не «низкая производительность».
Жюль
1
@Jules - спасибо за эти цифры. (любые источники?) Я могу им верить, потому что Java (и C #) должны перехватывать трассировку стека, что, безусловно , может показаться очень дорогим. Я все еще думаю, что ваш первоначальный ответ вводит в заблуждение, потому что даже 50-кратное замедление довольно тяжело, я думаю, особенно. в ориентированном на производительность языке, таком как C ++.
Мартин Ба,
2

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

Традиционный ответ, особенно когда был написан 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, если это значение или ошибка. А затем используйте say operator *для доступа к значению или какой-либо другой функции get. Обычно этот доступ не проверяется для скорости. Но вы можете сделать так, чтобы в отладочной сборке доступ становился проверенным, и утверждение гарантировало, что на самом деле есть значение, а не ошибка. Таким образом, любой, кто не проверяет ошибки должным образом, получит твердое утверждение, а не более коварную проблему.

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

Я стал большим поклонником этого стиля. Обычно я сейчас использую либо это, либо исключения. Но я стараюсь ограничить исключения основными проблемами. Для чего-то вроде ошибки синтаксического анализа, я пытаюсь вернуться, expected<T>например. Такие вещи, как std::stoiи boost::lexical_castкоторые выдают исключение C ++ в случае некоторой относительно незначительной проблемы «строка не может быть преобразована в число», кажутся мне сегодня очень плохим вкусом.

Крис Бек
источник
1
std::expectedвсе еще не принято предложение, верно?
Мартин Ба
Вы правы, я думаю, это еще не принято. Но есть несколько реализаций с открытым исходным кодом, и я, кажется, пару раз откатал свои собственные. Это менее сложно, чем сделать тип варианта, так как есть только два возможных состояния. Основные конструктивные соображения таковы: какой именно интерфейс вам нужен, и хотите ли вы, чтобы он соответствовал ожидаемому Андреску <T>, где объект ошибки фактически должен быть exception_ptr, или вы просто хотите использовать некоторый тип структуры или что-то еще? как это.
Крис Бек
Выступление Андрея Александреску здесь: channel9.msdn.com/Shows/Going+Deep/… Он подробно показывает, как создать такой класс и какие у вас могут быть соображения.
Крис Бек
Предлагаемый [[nodiscard]] attributeбудет полезен для этого подхода к обработке ошибок, поскольку он гарантирует, что вы не просто проигнорируете результат ошибки случайно.
CodesInChaos
- Да, я знал разговор А.А. Я нахожу дизайн довольно странным, поскольку для его распаковки ( except_ptr) вам пришлось вызвать внутреннее исключение. Лично я считаю, что такой инструмент должен работать совершенно независимо от исключений. Просто замечание.
Мартин Ба
1

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

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

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

Затем идет обновление ресурсов. Этот момент, по крайней мере для меня, тесно связан с аспектом критических операций приложения. Представьте себе Employeeкласс с функцией, UpdateDetails(std::string&)которая изменяет детали, основываясь на заданной строке через запятую. Подобно сбоям освобождения памяти, мне трудно представить, что присваивание значений переменных-членов терпит неудачу из-за отсутствия у меня опыта в таких областях, где это может произойти. Однако UpdateDetailsAndUpdateFile(std::string&)ожидается , что функция, подобная которой, как следует из названия, завершится с ошибкой. Это то, что я называю критической операцией.

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

Очевидно, что есть много критических операций, которые не привязаны к ресурсу. Если UpdateDetails()даны неверные данные, он не будет обновлять детали, и ошибка должна быть известна, так что вы бы выбросили здесь исключение. Но представьте себе такую ​​функцию, как GiveRaise(). Теперь, если упомянутому сотруднику повезло иметь заостренного босса и он не получит повышение (с точки зрения программирования, значение некоторой переменной не позволяет этому произойти), функция по существу потерпела неудачу. Вы бы бросили исключение здесь? Я хочу сказать, что вы должны оценить необходимость исключения.

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

Я вижу многих людей, которые в основном заменяют возвращаемые значения исключениями, потому что они используют C ++, а не C, и что это дает «хорошее разделение обработки ошибок» и т. Д., И призывают меня прекратить «смешивать» языки и т. Д. Я обычно держусь подальше от таких людей.

Vin
источник
1

Во- первых, как уже говорилось, все не что ясна в C ++, ИМХО в основном потому , что требования и ограничения являются несколько более варьировалась в C ++ , чем другие языки, особ. C # и Java, которые имеют "похожие" проблемы исключений.

Я покажу на примере std :: stof:

передача пустой строки в std :: stof (сгенерирует invalid_argument) не является ошибкой программирования

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

Здесь можно сказать, что logic_errorиспользуется, чтобы указать, что, учитывая, что (во время выполнения) ввод, всегда пытаться преобразовать его - ошибка, но задача функции - определить это и сообщить вам (через исключение).

Примечание: в этом представлении a runtime_error можно рассматривать как нечто, что при одинаковом входе в функцию теоретически может быть успешным для разных прогонов. (например, файловая операция, доступ к БД и т. д.)

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

Это просто показывает, IMHO, что группировка logic_или runtime_ошибка довольно нечетки в C ++ и не очень помогают в общем случае (*) - если вам нужно обрабатывать конкретные ошибки, вам, вероятно, нужно отлавливать меньше, чем две.

(*): Это не означает , что один кусок кода не должен быть последовательным, но будет ли вы бросить runtime_или logic_или custom_нечто действительно не так уж важно, как мне кажется.


Комментировать оба stofи bitset:

Обе функции принимают строки в качестве аргумента, и в обоих случаях это так:

  • нетривиально, чтобы проверить для вызывающей стороны, является ли данная строка допустимой (например, в худшем случае вам придется копировать логику функции; в случае набора битов не сразу ясно, является ли пустая строка допустимой, поэтому пусть решает ctor)
  • Функция уже несет ответственность за «синтаксический анализ» строки, поэтому она уже должна проверить строку, поэтому имеет смысл, что она сообщает об ошибке, чтобы «использовать» строку равномерно (и в обоих случаях это исключение) ,

Правило, которое часто встречается с исключениями: «Используйте исключения только в исключительных обстоятельствах». Но как библиотечная функция должна знать, какие обстоятельства являются исключительными?

Это утверждение имеет, ИМХО, два корня:

Производительность : если функция вызывается по критическому пути, и «исключительный» случай не является исключительным, т. Е. Значительное количество проходов будет связано с созданием исключения, то платить каждый раз за механизм исключения-разматывания не имеет смысла и может быть слишком медленным.

Местность обработки ошибок : Если функция вызывается и исключение сразу пойманы и обработаны, то есть мало смысла бросать исключение, так как обработка ошибок будет более многословным сcatch , чем с if.

Пример:

float readOrDefault;
try {
  readOrDefault = stof(...);
} catch(std::exception&) {
  // discard execption, just use default value
  readOrDefault = 3.14f; // 3.14 is the default value if cannot be read
}

Вот где такие функции, как TryParsevs., Parseвступают в игру: одна версия, когда локальный код ожидает, что проанализированная строка является действительной, одна версия, когда локальный код предполагает, что на самом деле ожидается (то есть, не является исключительным), что синтаксический анализ завершится неудачно.

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


Итак, как я должен решить, следует ли мне использовать исключения или нет для конкретной функции?

ИМХО, у вас есть два случая:

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

  • Функция «приложение» (специфичная для большого количества кода приложения, может быть использована повторно, но ограничена стилем обработки ошибок приложений и т. Д.): Здесь она часто должна быть достаточно четкой. Если пути кода, вызывающие функции, обрабатывают исключения разумным и полезным способом, используйте исключения, чтобы сообщить о любой (но см. Ниже) ошибке. Если код приложения легче читать и писать для стиля возврата ошибки, обязательно используйте его.

Конечно, между ними будут места - просто используйте то, что нужно, и помните YAGNI.


Наконец, я думаю, что я должен вернуться к утверждению FAQ,

Не используйте throw, чтобы указать на ошибку кодирования при использовании функции. Используйте assert или другой механизм для отправки процесса в отладчик или для сбоя процесса ...

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

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

Это возвращает нас к вопросу о том, нужно ли и как проверять предварительные условия вызова , но я не буду вдаваться в подробности, отвечу уже слишком долго :-)

Мартин Ба
источник