Подтвердить что ты не робот

Исключения и коды ошибок: их правильное смешивание

Я разрабатываю коммуникационную библиотеку С++. Библиотека предоставит унифицированный интерфейс для взаимодействия с различными ключами для выполнения удаленного кода, такими как SenseLock, KEYLOK, Guardant Code.

Ключи основаны на технологии смарт-карт и имеют внутреннюю файловую систему и оперативную память.

Типичная процедура работы включает (1) перечисление ключей, подключенных к USB-портам, (2) соединение с выбранным ключом, (3) выполнение именованного модуля, передающего входные данные и сбор выходных данных.

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

  • Ключ не найден (обязательно в фатальном случае).
  • Ошибка соединения с ключом (фатальный случай).
  • Указанный модуль выполнения не найден внутри ключа (?).
  • Запрошенная операция не удалась из-за таймаута (?).
  • Требуемая операция требует авторизации (предположительно восстанавливаемый случай).
  • Произошла ошибка памяти при выполнении модуля в ключе (обязательно в фатальном случае).
  • Произошла ошибка файловой системы в ключах (обязательно в фатальном случае).

? - Я еще не знаю, если дело считается фатальным или нет.

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

Вопросы:

  • Исключения заменяют коды ошибок полностью или, возможно, мне нужно использовать их только для "фатальных случаев"?
  • Является ли смешивание двух парадигм (исключений и кодов ошибок) хорошей идеей?
  • Это хорошая идея предоставить пользователю две концепции?
  • Есть ли хорошие примеры концепций смешивания исключений и ошибок?
  • Как бы вы это реализовали?

Обновление 1.

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

4b9b3361

Ответ 1

Если мы говорим о внутренней политике обработки ошибок приложения/модуля С++, то мое личное мнение:

Q:. Исключения заменяют коды ошибок полностью или, возможно, мне нужно использовать их только для "фатальных случаев"?

A: Они делают. Исключения всегда лучше для функции С++, чем возврат кодов ошибок. Причина объясняется ниже.

Q: Является ли смешивание двух парадигм (исключений и кодов ошибок) хорошей идеей?

A: Нет. Смешивание является уродливым и делает обработку ошибок непоследовательными. Когда я вынужден использовать API-интерфейс, возвращающий ошибки (например, API Win32, POSIX и т.д.), Я использую обматывающие пакеты исключений.

Q:. Это хорошая идея предоставить пользователю две концепции?

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

Q: Есть ли хорошие примеры концепций смешивания исключений и ошибок?

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

Q: Как вы это реализуете?

A: Я бы использовал только исключения. Мой путь возвращается только в случае успеха. Практика, возвращающая ошибки, сильно испортила код с ветвями проверки ошибок или еще хуже - проверка состояния ошибки отсутствует, и поэтому статус ошибки просто игнорируется, что делает код полным скрытых ошибок, которые трудно выявить. Исключения делают обработку ошибок изолированной. Если вам нужно обработать какую-то ошибку на месте, это обычно означает, что это не ошибка, а только какое-то законное событие, о котором можно сообщить успешным возвратом с определенным определенным статусом (по возврату или иным образом). Если вам действительно нужно проверить, произошла ли какая-либо ошибка локально (не из корневого блока try/catch), вы можете попробовать/поймать локально, поэтому использование только исключений действительно не ограничивает ваши возможности каким-либо образом.

Важное примечание:

Для каждой конкретной ситуации очень важно правильно определить, что такое ошибка, а что нет (для лучшего удобства использования).

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

В целом:

Исключения - более естественный механизм обработки ошибок для языка С++. Поэтому использование исключений - хорошая идея, если вы разрабатываете приложение или библиотеку С++ для использования только приложением С++ (а не с помощью приложения C и т.д.). Возврат ошибок - более переносимый подход - вы можете возвращать коды ошибок в приложения, написанные на любом языке программирования и даже работающие на разных компьютерах. Конечно, почти всегда запросы ОС сообщают о своем статусе с помощью кодов ошибок (потому что естественно сделать их независимыми от языка). И по этой причине вам приходится иметь дело с кодами ошибок в ежедневном программировании. НО IMO-планирование политики обработки ошибок приложения С++, основанного на кодах ошибок, просто требует неприятностей - приложение становится совершенно нечитаемым беспорядком. ИМО - лучший способ справиться с кодами состояния в приложении С++ - использовать функции/классы/методы оболочки С++ для вызова функции возврата ошибок и возврата ошибки - исключение throw (со статусной информацией, встроенной в класс исключений).

Некоторые важные примечания и оговорки:

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

Рассмотрим этот код:

SomeType* p = new SomeType;

some_list.push_back(p);
/* later element of some_list have to be delete-ed
   after removing them from this list */

Это типичная потенциальная утечка памяти - если push_back выбрасывает исключение, то динамически выделяется и конструируется объект SomeType.

Вариант безопасного исключения:

std::auto_ptr<SomeType> pa( new SomeType );

some_list.push_back(pa.get());
pa.release();
/* later elements of some_list have to be delete-ed
   after removing them from this list */

Или:

boost::shared_ptr<SomeType> pa( new SomeType );

some_list.push_back(pa);
/* some_list is list of boost::shared_ptr<SomeType>
   so everything is delete-ed automatically */

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

Ответ 2

Есть ли хорошие примеры концепций смешивания исключений и ошибок?

Да, boost.asio - это вездесущая библиотека, используемая для сетевой и последовательной связи на С++, и почти каждая функция поставляется в двух версиях: exception -рождение и возврат ошибок.

Например, iterator resolve(const query&) выдает boost::system::system_error при ошибке, а iterator resolve(const query&, boost::system::error_code & ec) изменяет ссылочный аргумент ec.

Конечно, что такое хороший дизайн для библиотеки, не является хорошим дизайном для приложения: приложение будет лучше всего использовать один подход последовательно. Тем не менее, вы создаете библиотеку, поэтому, если вы воспользуетесь им, использование boost.asio в качестве модели может быть работоспособной идеей.

Ответ 3

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

Я время от времени смешиваю коды ошибок и исключения. Вопреки некоторым другим ответам, я не думаю, что это "уродливый" или плохой дизайн. Иногда, неудобно, чтобы функция генерировала исключение при ошибке. Предположим, что вам все равно, если это не удастся:

DeleteFile(filename);

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

С другой стороны:

CreateDirectory(path);

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

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

Ответ 4

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

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

Естественно, здесь есть большая серая зона. Что часто и что далеко?

Я бы не рекомендовал вам предоставлять обе альтернативы, поскольку это в основном запутывает.

Ответ 5

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

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

В остальное время я использую типы, которые вставляют ошибки.

Простейшим является "необязательный" синтаксис. Если вы ищете объект в коллекции, вы можете его не найти. Здесь есть одна причина: это не в коллекции. Таким образом, код ошибки определенно подделка. Вместо этого можно использовать:

  • указатель (разделяемый, если вы хотите поделиться собственностью)
  • указательный объект (например, итератор)
  • объект с нулевыми значениями (kudos to boost::optional<> здесь)

Когда вещи более сложны, я склонен использовать "альтернативный" тип. Это идея типа Either в haskell, действительно. Либо вы возвращаете то, что попросил пользователь, либо вы возвращаете указание, почему вы его не вернули. boost::variant<> и сопровождающая boost::static_visitor<> игра здесь хорошо (в функциональном программировании это делается путем деконструкции объекта при сопоставлении с образцом).

Основная идея заключается в том, что код ошибки можно игнорировать, однако если вы возвращаете объект, являющийся результатом функции XOR, код ошибки, то его нельзя отменить (boost::variant<> и boost::optional<> действительно велики здесь).

Ответ 6

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

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

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

Ответ 7

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

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

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

Ответ 8

Исключения заменяют коды ошибок полностью или, возможно, мне нужно использовать их только для "фатальных случаев"?

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

Является ли смешивание двух парадигм (исключений и кодов ошибок) хорошей идеей?

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

Можно ли предложить пользователю две концепции?

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

Как вы это реализуете?

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

Ответ 9

Я не продаюсь, насколько хороша идея, но недавно я работал над проектом, где они не могли бросить вокруг исключений, но они не доверяли кодам ошибок. Поэтому они возвращают Error<T>, где T - любой тип кода ошибки, который они бы вернули (обычно и int какого-то типа, иногда строки). Если результат вышел из области действия без проверки, и произошла ошибка, исключение будет вызвано. Поэтому, если вы знали, что ничего не можете сделать, вы можете игнорировать ошибку и генерировать исключение, как ожидалось, но если вы можете что-то сделать, вы можете явно проверить результат.

Это была интересная смесь.

Этот вопрос продолжает появляться в верхней части активного списка, поэтому я полагаю, что немного поразработаю, как это работает. Класс Error<T> хранил исключение, стираемое стилем, поэтому его использование не заставляло использовать конкретную иерархию исключений или что-то в этом роде, и каждый отдельный метод мог бы выкинуть все количество исключений, которые ему нравились. Вы даже можете бросить int или что-то еще; почти что угодно с конструктором копирования.

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

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

Error<int> result = some_function();
do_something_independant_of_some_function();
if(result)
    do_something_else_only_if_some_function_succeeds();

Проверка if(result) гарантирует, что система ошибок перечисляет эту ошибку как обработанную, поэтому нет причин для result бросать свое хранимое исключение при уничтожении. Но если do_something_independant_of_some_function бросает, result будет уничтожен до достижения этой проверки. Это приводит к тому, что деструктор бросает второе исключение, и программа просто сдается и возвращается домой. Это очень легко избежать [всегда проверять результат функции перед тем, как делать что-либо еще], но все же рискованно.

Ответ 10

Исключения заменяют коды ошибок полностью или, возможно, мне нужно использовать их только для "фатальных случаев"?

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

  • EINTR: операция прерывается, иногда стоит перезапустить операцию, а в других случаях это означает, что "пользователь хочет выйти из программы"
  • EWOULDBLOCK: операция не блокируется, но теперь данные не могут быть переданы. Если вызывающий абонент ожидает надежного неблокирующего поведения (например, сокеты домена UNIX), это фатальная ошибка. Если вызывающий абонент проверяет оппортунистически, это пассивный успех.
  • частичный успех: в потоковом контексте (например, TCP, дисковый ввод-вывод) ожидаются частичные чтения/записи. В контексте дискретного обмена сообщениями (например, UDP) частичное чтение/запись может указывать на фатальное усечение.

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

Ответ 11

Храните исключения в локальной библиотеке/промежуточном программном обеспечении для вашей логики.

Вместо этого используйте коды ошибок для связи с клиентом.