Существует ли общепринятая идиома для обозначения кода C++, который может генерировать исключения?
Я видел проблемы при использовании кода C++, который неожиданно для вызывающей стороны выдает исключение. Не всегда возможно или практически невозможно прочитать каждую строку модуля, который вы используете, чтобы увидеть, генерирует ли он исключения, и если да, то какой тип исключения.
Существуют ли идиомы или "лучшие практики", которые существуют для решения этой проблемы?
Я думал о следующем:
В нашей документации по doxygen мы могли бы добавить комментарий к каждой функции, которая должна выдавать исключение, и его тип (ы).
- Плюсы: просто.
- Недостатки: возможны ошибки пользователя.
Мы могли бы иметь в приложении
try/catch(...)
для безопасности.- Плюсы: у нас не будет больше неучтенных исключений.
- Минусы: Исключение ловится далеко от броска. Трудно понять, что делать или что пошло не так.
Использовать спецификации исключений
- Плюсы: это санкционированный языком способ решения этой проблемы.
- Минусы: Рефакторинг проблемных библиотек, необходимых для того, чтобы это было эффективным. Не применяется во время компиляции, поэтому нарушения превращаются в проблемы времени выполнения, чего я стараюсь избегать!
Любой опыт с этими методами, или какие-либо дополнительные методы, о которых я не знаю?
11 ответов
Идиоматический способ решения проблемы состоит не в том, чтобы указать, что ваш код может генерировать исключения, а в обеспечении безопасности исключений в ваших объектах. Стандарт определяет несколько гарантий исключений, которые должны реализовывать объекты:
- Гарантия безброска: функция никогда не сгенерирует исключение
- Сильная гарантия безопасности исключения: если выбрасывается исключение, объект останется в своем первоначальном состоянии.
- Базовая гарантия безопасности исключений: если выдается исключение, объект остается в действительном состоянии.
И, конечно, стандарт документирует уровень безопасности исключений для каждого стандартного библиотечного класса.
Это действительно способ справиться с исключениями в C++. Вместо того, чтобы отмечать, какой код может или не может генерировать исключения, используйте RAII, чтобы гарантировать, что ваши объекты очищены, и подумайте о реализации соответствующего уровня безопасности исключений в ваших объектах RAII, чтобы они могли выжить без специальной обработки, если исключение брошено.
Исключения действительно вызывают проблемы, только если они позволяют вашим объектам оставаться в недопустимом состоянии. Этого никогда не должно случиться. Ваши объекты всегда должны реализовывать хотя бы основную гарантию. (и реализация класса контейнера, который обеспечивает надлежащий уровень безопасности исключений, является полезным упражнением на C++;))
Что касается документирования, когда вы можете точно определить, какие исключения может выдать функция, вы можете свободно документировать ее. Но в целом, когда ничего не указано, предполагается, что функция может выдать. Пустая спецификация броска иногда используется, чтобы документировать, когда функция никогда не бросает. Если его там нет, предположим, что функция может сгенерировать.
Короткий ответ на заглавный вопрос - идиома, указывающая на то, что функция может выдать, - это не документировать ее "эта функция не выбрасывает". То есть все может скинуть по умолчанию.
C++ не является Java и не имеет проверенных компилятором исключений. В C++ нет ничего, что позволяло бы компилятору сообщать вам, что ваш код утверждает, что он не сгенерирует, но вызывает то, что может. Таким образом, вы не можете полностью избежать этой проблемы во время выполнения. Инструменты статического анализа могут помочь, не уверен.
Если вы заботитесь только о MSVC, вы можете использовать пустую спецификацию исключений или __declspec(nothrow)
на функции, которые не бросают, и throw(...)
на функции, которые делают. Это не приведет к неэффективному коду, потому что MSVC не испускает код, чтобы проверить, что функции, объявленные nothrow, на самом деле не генерируют. GCC может сделать то же самое с -fno-enforce-eh-specs
, проверьте документацию вашего компилятора. Тогда все будет автоматически задокументировано.
Вариант 2, try-catch для всего приложения на самом деле не "для безопасности", это просто потому, что вы думаете, что вы можете сделать что-то более полезное с исключением (например, распечатать что-то и выйти чисто), чем просто позволить вызову времени выполнения C++ terminate
, Если вы пишете код в предположении, что что-то не сработает, и это действительно происходит, то вы, возможно, остались неопределенными до того, как произойдет одно из них, например, если деструктор делает ложное предположение о согласованном состоянии.
Я бы обычно делал вариант (1): для каждого функционального документа, какую гарантию исключения он предлагает - nothrow, strong, слабый или нет. Последняя ошибка. Первый ценится, но редко, и при хорошем кодировании строго необходим только для функций подкачки и деструкторов. Да, он подвержен ошибкам пользователя, но любые средства кодирования C++ с исключениями подвержены ошибкам пользователя. Кроме того, также выполните (2) и / или (3), если это поможет вам обеспечить соблюдение (1).
Symbian имеет предстандартный диалект C++ с механизмом, называемым "отпуск", который в некоторых отношениях похож на исключения. Соглашение в Symbian состоит в том, что любая функция, которая может выйти, должна иметь имя с L в конце: CreateL
, ConnectL
и т. д. В среднем это уменьшает пользовательскую ошибку, потому что вам легче увидеть, вызываете ли вы что-нибудь, что может уйти. Как и следовало ожидать, те же люди ненавидят тех, кто ненавидит венгерскую нотацию приложений, и если почти все функции покидают ее, она перестает быть полезной. И, как вы можете ожидать, если вы напишете функцию, которая не будет содержать L в названии, вам может понадобиться долгое время в отладчике, прежде чем вы обнаружите проблему, потому что ваши предположения уводят вас от реальной ошибки.
Честно говоря, почти любая функция C++ может вызвать исключение. Вы не должны слишком беспокоиться о документировании этого, но вместо этого сделайте свой код безопасным, используя такие идиомы, как RAII.
C++, до C++11, определил спецификацию броска. Смотрите этот вопрос. В прошлом я видел, что компиляторы Microsoft игнорируют спецификацию C++ throw. Само понятие "проверенные исключения" является спорным, особенно в сфере Java. Многие разработчики считают это неудачным экспериментом.
Документация, кажется, единственный разумный метод, о котором я знаю.
Что касается спецификаций исключений, вот старая (2002 г.) статья Херба Саттера на эту тему: http://www.ddj.com/cpp/184401544 Здесь обсуждается, почему эта особенность языка не дает нам безопасности во время компиляции и заканчивается с выводом:
Итак, вот что, кажется, лучший совет, который мы, как сообщество, получили на сегодняшний день:
Мораль № 1: Никогда не пишите спецификацию исключений.
Мораль № 2: За исключением, возможно, пустого, но на вашем месте я бы избегал даже этого.
Я использую как 1, так и 2.
О 1: Вы не можете избежать или предотвратить ошибку пользователя. Вы можете победить разработчика, который неправильно записал doxygen в целлюлозу, если вы его знаете. Но вы не можете избежать или предотвратить ошибку пользователя, поэтому избавьтесь от паранойи. Если пользователь ошибся, он сделал это, а не вы.
О 2: C# имеет встроенный способ отловить необработанные исключения. Так что это не "плохо", хотя я согласен, что пахнет. Иногда лучше просто аварийно завершить работу, чем работать непоследовательно, но я применил практику регистрации любых необработанных исключений и ТОГО сбоя. Это позволяет людям отправлять мне журнал, чтобы я мог проверить трассировку стека и отследить проблему. Таким образом, после каждого исправления происходит все меньше и больше сбоев.
Документируйте, какой уровень безопасности исключений гарантирует функция. Как указал Стив Джессоп в своем ответе, существует несколько уровней. В идеале, если они документированы для всех интерфейсов или, по крайней мере, необходимого минимума: а) никогда не выбрасывает или б) может бросить
Читайте об исключительных гарантиях безопасности Абрахамса, объясненных Хербом Саттером.
Я сильно сомневаюсь, что это будет практично в большой кодовой базе. Относительно далеко от броска, и трудно понять, что делать, проблемы, хорошее правило - ловить только в тех местах, где вы хотите иметь дело с исключением, в противном случае пусть это всплывет. Не стоит ловить и гасить исключения, как только они появляются, просто потому, что... они являются исключениями, поэтому вы чувствуете, что должны что-то с этим делать. В сложных системах хорошо иметь механизм регистрации, чтобы легче было отследить проблему.
Не делай этого. Прочитайте Херб Саттер Прагматичный взгляд на спецификации исключений и соответствующие статьи.
Используйте документацию doxygen для описания ваших методов. Когда вы их используете, вам нужно будет проверить эту документацию, чтобы увидеть, каковы их параметры и какие исключения они выдают.
Напишите модульные тесты для проверки вашего кода в тех случаях, когда генерируются исключения.
Нет.
Единственная распространенная вещь - указывать, что вы ничего не бросаете.
И затем вы также должны вручную убедиться, что никакое исключение не может на самом деле избежать вашего метода / функции. Примечание. Если исключения исключают метод, приложение будет закрыто.
#include <stdexcept>
class MyException: public std::exception
{
public:
~MyException() throw() {}
char* what() const throw()
{
try
{
// Do Stuff that may throw
return "HI";
}
// Exceptions must not escape this method
// So catch everything.
catch(...)
{
return "Bad Stuff HAppening Cant Make Exception Message.";
}
}
};
Вы всегда должны ожидать исключений и обрабатывать их. К сожалению, для компьютера нет автоматизированного способа провести комплексную проверку.
Есть способ указать, какие исключения функция может генерировать в C++. Пример взят из книги Страуструпа:
void f(int a) throw (x2, x3);
который указывает, что f может генерировать только исключения типа x2 или x3 или производных типов. Используя этот синтаксис, вы можете легко просмотреть декларацию функций, чтобы увидеть, какие исключения разрешено выдавать по закону, и соответствующим образом кодировать.