Операторы try, throw и catch (C++)
Для реализации обработки исключений в C++используется try throw и catch выражения.
Во-первых, используйте try блок для заключения одного или нескольких операторов, которые могут вызвать исключение.
throw Выражение сигнализирует о том, что исключительное условие ( часто ошибка) произошла в блоке try . Объект любого типа можно использовать в качестве операнда throw выражения. Обычно этот объект используется для передачи информации об ошибке. В большинстве случаев рекомендуется использовать std::exception класс или один из производных классов, определенных в стандартной библиотеке. Если один из них не подходит, рекомендуется наследовать собственный класс исключений. std::exception
Для обработки исключений, которые могут быть возникают, реализуйте один или несколько catch блоков сразу после try блока. Каждый catch блок указывает тип исключения, который он может обрабатывать.
В этом примере показан try блок и его обработчики. Предположим, GetNetworkResource() получает данные через сетевое подключение, а 2 типа исключений являются определенными пользователем классами, производными от std::exception . Обратите внимание, что исключения перехватываются по const ссылке в инструкции catch . Рекомендуется создавать исключения по значению и захватывать их ссылкой константы.
Пример
MyData md; try < // Code that could throw an exception md = GetNetworkResource(); >catch (const networkIOException& e) < // Code that executes when an exception of type // networkIOException is thrown in the try block // . // Log error message in the exception object cerr catch (const myDataFormatException& e) < // Code that handles another exception type // . cerr // The following syntax shows a throw expression MyData GetNetworkResource() < // . if (IOSuccess == false) throw networkIOException("Unable to connect"); // . if (readError) throw myDataFormatException("Format error"); // . >
Замечания
Код после try предложения является защищенным разделом кода. Выражение throw вызывает исключение. Блок кода после catch предложения является обработчиком исключений. Это обработчик, который перехватывает исключение, которое создается, если типы в throw выражениях catch совместимы. Список правил, которые управляют сопоставлением типов в catch блоках, см. в разделе «Оценка блоков catch». catch Если инструкция задает многоточие (. ) вместо типа, catch блок обрабатывает каждый тип исключения. При компиляции с /EHa параметром они могут включать структурированные исключения C и системные или асинхронные исключения, созданные приложением, такие как защита памяти, разделение на ноль и нарушения с плавающей запятой. Так как catch блоки обрабатываются в программе, чтобы найти соответствующий тип, обработчик многоточия должен быть последним обработчиком для связанного try блока. Используйте с осторожностью; не позволяйте catch(. ) программе продолжать работу, если блок catch не знает, как обрабатывать определенное исключение, которое поймано. Как правило, блок catch(. ) используется для ведения журнала ошибок и выполнения специальной очистки перед остановкой выполнения программы.
Выражение throw , которое не имеет операнда повторно выполняет обработку исключения. Мы рекомендуем эту форму при повторном создании исключения, так как это сохраняет сведения о полиморфном типе исходного исключения. Такое выражение должно использоваться только в catch обработчике или в функции, которая вызывается из обработчика catch . Объект исключения rethrown является исходным объектом исключения, а не копией.
try < throw CSomeOtherException(); >catch(. ) < // Catch all exceptions - dangerous. // Respond (perhaps only partially) to the exception, then // re-throw to pass the exception to some other handler // . throw; >
Throw c что это
Обычно система сама генерирует исключения при определенных ситуациях, например, при делении числа на ноль. Но язык C# также позволяет генерировать исключения вручную с помощью оператора throw . То есть с помощью этого оператора мы сами можем создать исключение и вызвать его в процессе выполнения.
Например, в нашей программе происходит ввод имени пользователя, и мы хотим, чтобы, если длина имени меньше 2 символов, то возникало исключение:
try < Console.Write("Введите имя: "); string? name = Console.ReadLine(); if (name== null || name.Length < 2) < throw new Exception("Длина имени меньше 2 символов"); >else < Console.WriteLine($"Ваше имя: "); > > catch (Exception e) < Console.WriteLine($"Ошибка: "); >
После оператора throw указывается объект исключения, через конструктор которого мы можем передать сообщение об ошибке. Естественно вместо типа Exception мы можем использовать объект любого другого типа исключений.
Затем в блоке catch сгенерированное нами исключение будет обработано.
Подобным образом мы можем генерировать исключения в любом месте программы. Но существует также и другая форма использования оператора throw, когда после данного оператора не указывается объект исключения. В подобном виде оператор throw может использоваться только в блоке catch:
try < try < Console.Write("Введите имя: "); string? name = Console.ReadLine(); if (name == null || name.Length < 2) < throw new Exception("Длина имени меньше 2 символов"); >else < Console.WriteLine($"Ваше имя: "); > > catch (Exception e) < Console.WriteLine($"Ошибка: "); throw; > > catch (Exception ex)
В данном случае при вводе имени с длиной меньше 2 символов возникнет исключение, которое будет обработано внутренним блоком catch. Однако поскольку в этом блоке используется оператор throw, то исключение будет передано дальше внешнему блоку catch, который получит то же самое исключение и выведет то же самое сообщение на консоль.
Спецификации исключений (throw, noexcept) (C++)
Спецификации исключений — это функция языка C++, указывающая намерение программиста о типах исключений, которые могут распространяться функцией. Можно указать, что функция может или не выйти из исключения с помощью спецификации исключения. Компилятор может использовать эти сведения для оптимизации вызовов функции и завершения программы, если непредвиденное исключение выходит из функции.
До C++17 существовали два типа спецификации исключений. Спецификация noexcept была новой в C++11. Он указывает, является ли набор потенциальных исключений, которые могут экранировать функцию, пуста. Спецификация динамического исключения или throw(optional_type_list) спецификация устарела в C++11 и удалена в C++17, за исключением throw() псевдонима noexcept(true) . Эта спецификация исключений была разработана для предоставления сводных сведений о том, какие исключения могут быть выброшены из функции, но на практике было обнаружено, что это проблематично. Одна спецификация динамического исключения, которая оказалась несколько полезной, была безусловной throw() спецификацией. Например, объявление функции:
void MyFunction(int i) throw();
сообщает компилятору, что функция не создает исключений. Однако в /std:c++14 режиме это может привести к неопределенному поведению, если функция создает исключение. Поэтому рекомендуется использовать noexcept оператор вместо приведенного выше:
void MyFunction(int i) noexcept;
В следующей таблице приведены сведения о реализации спецификаций исключений Microsoft C++:
Спецификация исключений | Значение |
---|---|
noexcept noexcept(true) throw() |
Функция не вызывает исключений. В /std:c++14 режиме (по умолчанию) noexcept и noexcept(true) эквивалентны. При возникновении исключения из функции, объявленной noexcept или noexcept(true) std::terminate вызываемой. Если исключение создается из функции, объявленной как throw() в /std:c++14 режиме, результат не определен. Не вызывается определенная функция. Это расхождение со стандартом C++14, которое требуется компилятору для вызова std::unexpected . Visual Studio 2017 версии 15.5 и более поздних версий: в /std:c++17 режиме , noexcept noexcept(true) и throw() все эквивалентны. В /std:c++17 режиме throw() является псевдонимом для noexcept(true) . В /std:c++17 режиме и более поздних версиях, когда исключение создается из функции, объявленной с любой из этих спецификаций, std::terminate вызывается в соответствии со стандартом C++17. |
noexcept(false) throw(. ) Нет спецификации |
Функция может вызывать исключение любого типа. |
throw(type) | (C++14 и более ранних версий) Функция может вызывать исключение типа type . Компилятор принимает синтаксис, но интерпретирует его как noexcept(false) . В /std:c++17 режиме и более поздних версиях компилятор выдает предупреждение C5040. |
Если обработка исключений используется в приложении, в стеке вызовов должна быть функция, которая обрабатывает исключения, прежде чем они выходят из внешней область функции, помеченной noexcept или noexcept(true) throw() . Если какие-либо функции, вызываемые между тем, который вызывает исключение, и тот, который обрабатывает исключение, указывается как noexcept noexcept(true) (или throw() в /std:c++17 режиме), программа завершается, когда функция noexcept распространяет исключение.
Поведение исключения функции зависит от следующих факторов:
- Какой языковой стандартный режим компиляции задан.
- В какой среде выполняется компиляция функции — в C или C++.
- Какой /EH параметр компилятора вы используете.
- Задана ли явно спецификация исключений.
Явные спецификации исключений не разрешено использовать для функций C. Предполагается, что функция C не создает исключения под , и может вызывать структурированные /EHsc исключения в /EHs , /EHa или /EHac .
В следующей таблице приводится сводка о том, может ли функция C++ потенциально вызывать различные параметры обработки исключений компилятора:
Функция | /EHsc | /EHs | /EHa | /EHac |
---|---|---|---|---|
Функция C++ без спецификации исключений | Да | Да | Да | Да |
Функция C++ с noexcept спецификацией исключений noexcept(true) , или throw() спецификацией исключений | No | No | Да | Да |
Функция C++ с noexcept(false) спецификацией исключений throw(. ) , или throw(type) спецификацией исключений | Да | Да | Да | Да |
Пример
// exception_specification.cpp // compile with: /EHs #include void handler() < printf_s("in handler\n"); >void f1(void) throw(int) < printf_s("About to throw 1\n"); if (1) throw 1; >void f5(void) throw() < try < f1(); >catch(. ) < handler(); >> // invalid, doesn't handle the int exception thrown from f1() // void f3(void) throw() < // f1(); // >void __declspec(nothrow) f2(void) < try < f1(); >catch(int) < handler(); >> // only valid if compiled without /EHc // /EHc means assume extern "C" functions don't throw exceptions extern "C" void f4(void); void f4(void) < f1(); >int main() < f2(); try < f4(); >catch(. ) < printf_s("Caught exception from f4\n"); >f5(); >
About to throw 1 in handler About to throw 1 Caught exception from f4 About to throw 1 in handler
Обработка исключений в C++
Язык С представляет программисту очень ограниченные возможности обработки исключений, возникших при работе программы. В этом отношении С++ намного развитее С. Здесь у программиста существенно большие возможности по непосредственной обработке исключений. Комитет по разработке стандартов С++ предоставил очень простую, но мощную форму обработки исключений.
Темные дни С
Типичная функция, написанная на С, выглядит примерно так:
long DoSomething() < long *a, c; FILE *b; a = malloc(sizeof(long) * 10); if (a == NULL) return 1; b = fopen("something.bah", "rb"); if (b == NULL) < free(a); return 2; >fread(a, sizeof(long), 10, b); if (a[0] != 0x10) < free(a); fclose(b); return 3; >fclose(b); c = a[1]; free(a); return c; >
Выглядит не очень, не так ли? Вы целиком и полностью зависите от значений, которые возвращают вам функции и для каждой ошибки вам постоянно нужен код, который ее обрабатывает. Если вы, скажем, в функции работаете хотя бы с 10 указателями (рапределяете память, освобождаете ее и т.д.), то наверняка половину кода функции будет занимать код обработки ошибок. Такая же ситуация будет в коде, вызывающем эту функцию, так как здесь также нужно обработать все возвращаемые коды ошибок.
Try-catch-throw
Давайте же разберем основы обработки исключений в С++. Чтобы комфортно работать с исключениями в С++ вам нужно знать лишь три ключевых слова:
- try (пытаться) — начало блока исключений;
- catch (поймать) — начало блока, «ловящего» исключение;
- throw (бросить) — ключевое слово, «создающее» («возбуждающее») исключение.
А теперь пример, демонстрирующий, как применить то, что вы узнали:
void func() < try < throw 1; >catch(int a) < cout cout
Если выполнить этот фрагмент кода, то мы получим следующий результат:
Caught exception number: 1
Теперь закоментируйте строку throw 1; и функция выдаст такой результат:
No exception detected!
Как видите все очень просто, но если это применить с умом, такой подход покажется вам очень мощным средством обработки ошибок. Catch может «ловить» любой тип данных, так же как и throw может «кинуть» данные любого типа. Т.е. throw AnyClass(); будет правильно работать, так же как и catch (AnyClass &d) <>;.
Как уже было сказано, catch может «ловить» данные любого типа, но вовсе не обязательно при это указывать переменную. Т.е. прекрасно будет работать что-нибудь типа этого:
catch(dumbclass)
catch(dumbclass&)
Так же можно «поймать» и все исключения:
catch(. )
Троеточие в этом случае показывает, что будут пойманы все исключения. При таком подходе нельзя указать имя переменной. В случае, если «кидаются» данные нестандартного типа (экземпляры определенных вами классов, структур и т.д.), лучше «ловить» их по ссылке, иначе вся «кидаемая» переменная будет скопирована в стек вместо того, чтобы просто передать указатель на нее. Если кидаются данные нескольких типов и вы хотите поймать конкретную переменную (вернее, переменную конкретного типа), то можно использовать несколько блоков catch, ловящих «свой» тип данных:
try < throw 1; // throw 'a'; >catch (long b) < cout catch (char b) < cout "
Создание" исключений
Когда возбуждается исключительная ситуация, программа просматривает стек функций до тех пор, пока не находит соответствующий catch. Если оператор catch не найден, STL будет обрабатывать исключение в стандартном обработчике, который делает все менее изящно, чем могли бы сделать вы, показывая какие-то непонятные (для конечного пользователя) сообщения и обычно аварийно завершая программу.
Однако более важным моментом является то, что пока просматривается стек функций, вызываются деструкторы всех локальных классов, так что вам не нужно забодиться об освобождении памяти и т.п.
Перегрузка глобальных операторов new/delete
А сейчас хотелось бы отправить вас к статье "Как обнаружить утечку памяти". В ней рассказывается, как обнаружить неправильное управление распределением памяти в вашей программе. Вы можете спросить, при чем тут перегрузка операторов? Если перегрузить стандартные new и delete, то открываются широкие возможности по отслеживанию ошибок (причем ошибок часто критических) с помощью исключений. Например:
char *a; try < a = new char[10]; >catch (. ) < // a не создан - обработать ошибку распределения памяти, // выйти из программы и т.п. >// a успешно создан, продолжаем выполнение
Это, на первый взгляд, кажется длиннее, чем стандартная проверка в С "а равен NULL?", однако если в программе выделяется десяток динамических переменных, то такой метод оправдывает себя.
Операторы throw без параметров
Итак, мы увидели, как новый метод обработки ошибок удобен и прост. Блок try-catch может содержать вложенные блоки try-catch и если не будет определено соответствующего оператора catch на текущем уровен вложения, исключение будет поймано на более высоком уровне. Единственная вещь, о которой вы должны помнить, - это то, что операторы, следующие за throw, никогда не выполнятся.
try < throw; // ни один оператор, следующий далее (до закрывающей скобки) // выполнен не будет >catch(. )
Такой метод может применяться в случаях, когда не нужно передавать никаких данных в блок catch.
Приложение
Приведем пример, как все вышеизложенное может быть использовано в конкретном приложении. Преположим, у вас в программе есть класс cMain и экземпляр этого класса Main: class cMain < public: bool Setup(); bool Loop(); // Основной цикл программы void Close(); >; cMain Main;
А в функции main() или WinMain() вы можете использовать этот класс как-нибудь так:
try < Main.Setup(); Main.Loop(); Main.Close(); >catch (Exception &e) < // использование класса, ведущего лог. log("Exception thrown: %s", e.String()); // Показываем сообщение об ошибке и закрываем приложение. >
Основной цикл программы может выглядеть примерно так:
while (AppActive) < try < // какие-то действия >catch (Exception &e) < /* Если исключение критическое, типа ошибки памяти, посылаем исключение дальше, в main(), оператором throw e; или просто throw. Если исключение некритично, обрабатываем его и возвращаемся в основной цикл. */ >>
Заключение
Метод обработки исключений, приведенный в статье, является удобным и мощным средством, однако только вам решать, использовать его или нет. Одно можно скачать точно - приведенный метод облегчит вам жизнь. Если хотите узнать об исключениях чуть больше, посмотрите публикацию Deep C++ на сервере MSDN.