Исключения
В процессе работы программы могут возникать различные ошибки. Например, при передаче файла по сети оборвется сетевое подключение или будут введены некорректные и недопустимые данные, которые вызовут падение программы. Такие ошибки еще называются исключениями. Исключение представлякт временный объект любого типа, который используется для сигнализации об ошибке. Цель объекта-исключения состоит в том, чтобы передать информацию из точки, в которой произошла ошибка, в код, который должен ее обработать. Если исключение не обработано, то при его возникновении программа прекращает свою работу.
Например, в следующей программе происходит деление чисел:
#include double divide(int a, int b) < return a / b; >int main() < int x; int y<>; double z
Эта программа успешно скомпилируется, но при ее выполнении возникнет ошибка, поскольку в коде производится деление на ноль, после чего программа аварийно завершится.
С одной стороны, мы можем в функции divide определить проверку и выполнять деление, если параметр b не равен 0. Однако нам в любом случае надо возвращать из функции divide некоторый результат — некоторое число. То есть мы не можем просто написать:
double divide(int a, int b) < if (b) return a / b; else std::cout
И в этом случае нам надо известить систему о возникшей ошибке. Для этого используется оператор throw .
Оператор throw генерирует исключение. Через оператор throw можно передать информацию об ошибке. Например, функция divide могла бы выглядеть следующим образом:
double divide(int a, int b) < if (b) return a / b; throw "Division by zero!"; >
То есть если параметр b равен 0, то генерируем исключение.
Но это исключение еще надо обработать в коде, где будет вызываться функция divide. Для обработки исключений применяется конструкция try. catch . Она имеет следующую форму:
try < инструкции, которые могут вызвать исключение >catch(объявление_исключения)
В блок кода после ключевого слова try помещается код, который потенциально может сгенерировать исключение.
После ключевого слова catch в скобках идет параметр, который передает информацию об исключении. Затем в блоке производится собственно обработка исключения.
Так изменим весь код следующим образом:
#include double divide(int a, int b) < if (b) return a / b; throw "Division by zero!"; >int main() < int x; int y<>; try < double z Код, который потенциально может сгенерировать исключение — вызов функции divide помещается в блок try. В блоке catch идет обработка исключения. Причем многоточие в скобках после оператора catch ( catch(. ) ) позволяет обработать любое исключение. В итоге когда выполнение программы дойдет до строки При выполнении этой строки будет сгенерировано исключение, поэтому последующие инструкции из блока try выполняться не будут, а управление перейдет в блок catch, в котором на консоль просто выводится сообщение об ошибке. После выполнения блока catch программа аварийно не завершится, а продолжит свою работу, выполняя операторы после блока catch: Однако в данном случае мы только знаем, что произошла какая-то ошибка, а какая именно, неизвестно. Поэтому через параметр в блоке catch мы можем получить то сообщение, которое передается оператору throw : #include >iostream < double divide(int a, int b) < if (b) return a / b; throw "Division by zero!"; >int main() < int x; int y<>; try < double z С помощью параметра const char* error_message получаем сообщение, которое предано оператору throw, и выводим это сообщение на консоль. Почему здесь мы получаем сообщение об ошибке в виде типа const char* ? Потому что после оператора throw идет строковый литерал, который представляет как раз тип const char* . И в этом случае консольный вывод будет выглядеть следующим образом: Таким образом, мы можем узнать суть возникшего исключения. Подобным образом мы можем передавать информацию об исключении через любые типы, например, std::string: Тогда в блоке catch мы можем получить эту информацию в виде объекта std::string : catch (std::string error_message) Если же исключение не обработано, то вызывается функция std::terminate() (из модуля стандартной библиотеки C++), которая, в свою очередь, по умолчанию вызывает другую функцию — std::abort() (из ), которая собственно и завершает программу. Существует очень много функций и в стандартной библиотеке С++, и в каких-то сторонних библиотеках. И может возникнуть вопрос, какие из них вызывать в конструкции try-catch, чтобы не столкнуться с необработанным исключением и аварийным завершением программы. В этом случае может помочь прежде всего документация по функции (при ее наличии). Другой сигнал — ключевое слово noexcept , которое при использовании в заголовке функции указывает, что эта функция никогда не будет генерировать исключения. Например: Здесь указываем, что функция print() никогда не вызовет исключение. Таким образом, встретив функцию с подобным ключевым словом, можно ожидать, что она не вызовет исключения. И соответственно нет необходимости помещать ее вызов в конструкцию try-catch. При обработке исключения стоит помнить, что при передаче объекта оператору throw блок catch получает копию этого объекта. И эта копия существует только в пределах блока catch. Для значений примитивных типов, например, int , копирование значения может не влиять на производительность программы. Однако при передаче объектов классов издержки могут выше. Поэтому в этом случае объекты обычно передаются по ссылке, например: #include double divide(int a, int b) < if (b) return a / b; throw std::string; > int main() < int x; int y<>; try < double z Мы можем генерировать и обрабатывать несколько разных исключительных ситуаций. Допустим, нам надо, чтобы при делении делитель был меньше делимого: В функции divide в зависимости от значения числа b оператору throw передаем либо число: либо строковый литерал: Для тестирования функции divide определена другая функция — test, где вызов функции divide() помещен в конструкцию try..catch . Поскольку при генерации исключения мы можем получать ошибку в виде двух типов — int (если b равно 0) и const char* (если b больше a), то для обработки каждого типа исключений определены два разных блока catch: catch (int code) < std::cout catch (const char* error_message)
В функции main вызываем функцию test, передавая в нее различные числа. При вызове: число b не равно 0 и меньше a, поэтому никаких исключений не возникает, блок try срабатывает до конца, и функция завершает свое выполнение. При втором вызове число b равно 0, поэтому генерируется исключение, а в качестве объекта исключения передается число 0. Поэтому при возникновении исключения программа выберет тот блок catch, где обрабатывается исключения типа int: catch (int code) При третьем вызове число b больше a, поэтому объект исключения будет представлять строковый литерал или const char* . Поэтому при возникновении исключения программа выберет блок catch, где обрабатывается исключения типа const char*: catch (const char* error_message) Таким образом, в данном случае мы получим следующий консольный вывод: Может быть ситуация, когда генерируется исключение внутри конструкции try-catch , и даже есть блок catch для обработки исключений, однако он обрабатывает другие типы исключений: Здесь нет блока catch для обработки исключения типа int . Поэтому при генерации исключения: Программа не найдет нужный блок catch для обработки исключения, и программа аварийно завершит свое выполнение. Стоит отметить, что, если в блоке try создаются некоторые объекты, то при возникновении исключения у них вызываются деструкторы. Например: В классе Person определяет деструктор, который выводит сообщение на консоль. В функции print просто генерируем исключение. В функции main в блоке try создаем один объект Person и вызываем у него функцию print, что естественно приведет к генерарции исключения и переходу управления программы в блок catch . И если мы посмотрим на консольный вывод то мы увидим, что прежде чем начнется обработка исключения в блоке catch, будет вызван деструктор объекта Person. Разберёмся, для чего нужны исключения в языке C++ и какими они бывают. Изучим синтаксис выбрасывания, обработки и рассмотрим особые случаи. Поговорим об исключениях в C++, начиная определением и заканчивая грамотной обработкой. Георгий Осипов Один из авторов курса «Разработчик C++» в Яндекс Практикуме, разработчик в Лаборатории компьютерной графики и мультимедиа ВМК МГУ Исключения — важный инструмент в современном программировании. В большинстве источников тема исключений раскрывается не полностью: не описана механика их работы, производительность или особенности языка C++. В статье я постарался раскрыть тему исключений достаточно подробно. Она будет полезна новичкам, чтобы узнать об исключениях, и программистам с опытом, чтобы углубиться в явление и достичь его полного понимания. Статья поделена на две части. Первая перед вами и содержит базовые, но важные сведения. Вторая выйдет чуть позже. В ней — информация для более продвинутых разработчиков. В первой части разберёмся: Также рассмотрим основные стандартные типы исключений, где и для чего они применяются. Мы опираемся на современные компиляторы и Стандарт C++20. Немного затронем C++23 и даже C++03. Если вы только осваиваете C++, возможно, вам будет интересен курс «Разработчик C++» в Яндекс Практикуме. У курса есть бесплатная вводная часть. Именно она может стать вашим первым шагом в мир C++. Для тех, кто знаком с программированием, есть внушительная ознакомительная часть, тоже бесплатная. В жизни любой программы бывают моменты, когда всё идёт не совсем так, как задумывал разработчик. Например: Примеры объединяет одно: возникшая ситуация достаточно редка, и при нормальной работе программы, всех устройств, сети и адекватном поведении пользователя она не возникает. Хороший программист старается предусмотреть подобные ситуации. Однако это бывает сложно: перечисленные проблемы обладают неприятным свойством — они могут возникнуть практически в любой момент. На помощь программисту приходят исключения (exception). Так называют объекты, которые хранят данные о возникшей проблеме. Механизмы исключений в разных языках программирования очень похожи. В зависимости от терминологии языка исключения либо выбрасывают (throw), либо генерируют (raise). Это происходит в тот момент, когда программа не может продолжать выполнять запрошенную операцию. После выбрасывания в дело вступает системный код, который ищет подходящий обработчик. Особенность в том, что тот, кто выбрасывает исключение, не знает, кто будет его обрабатывать. Может быть, что и вовсе никто — такое исключение останется сиротой и приведёт к падению программы. Если обработчик всё же найден, то он ловит (catch) исключение и программа продолжает работать как обычно. В некоторых языках вместо catch используется глагол except (исключить). Обработчик ловит не все исключения, а только некоторые — те, что возникли в конкретной части определённой функции. Эту часть нужно явно обозначить, для чего используют конструкцию try (попробовать). Также обработчик не поймает исключение, которое ранее попало в другой обработчик. После обработки исключения программа продолжает выполнение как ни в чём не бывало. Перед тем как совершить операцию, нужно убедиться, что она корректна. Если да — совершить эту операцию, а если нет — выбросить исключение. Так делается в некоторых языках, но не в C++. Проверка корректности — это время, а время, как известно, деньги. В C++ считается, что программист знает, что делает, и не нуждается в дополнительных проверках. Это одна из причин, почему программы на C++ такие быстрые. Но за всё нужно платить. Если вы не уследили и сделали недопустимую операцию, то в менее производительных языках вы получите исключение, а в C++ — неопределённое поведение. Исключение можно обработать и продолжить выполнение программы. Неопределённое поведение гарантированно обработать нельзя. Но некоторые виды неопределённого поведения вполне понятны и даже могут быть обработаны. Это зависит от операционной системы: Особенность C++ в том, что не любая ошибка влечёт исключение, и не любую ошибку можно обработать. Но если для операции производительность не так критична, почему бы не сделать проверку? У ряда операций в C++ есть две реализации. Одна супербыстрая, но вы будете отвечать за корректность, а вторая делает проверку и выбрасывает исключение в случае ошибки. Например, к элементу класса std::vector можно обратиться двумя способами: В C++ вам даётся выбор: делать быстро или делать безопасно. Часто безопасность важнее, но в определённых местах программы любое промедление критично. Начнём с примера: В примере есть один try -блок и один catch -блок. Если в блоке try возникает исключение типа ExceptionType , то выполнение блока заканчивается. При этом корректно удаляются созданные объекты — в данном случае переменная var . Затем управление переходит в конструкцию catch . Сам объект исключения передаётся в переменную e . Выводя e.what() , мы предполагаем, что у типа ExceptionType есть метод what . Если в блоке try возникло исключение другого типа, то управление также прервётся, но поиск обработчика будет выполняться за пределами функции SomeFunction — выше по стеку вызовов. Это также касается любых исключений, возникших вне try -блока. Во всех случаях объект var будет корректно удалён. Исключение не обязано возникнуть непосредственно внутри DoSomething*() . Будут обработаны исключения, возникшие в функциях, вызванных из DoSomething* , или в функциях, вызванных из тех функций, да и вообще на любом уровне вложенности. Главное, чтобы исключение не было обработано ранее. Можно указать несколько блоков catch , чтобы обработать исключения разных типов: Если перед catch(. ) есть другие блоки, то он означает «поймать все остальные исключения». Ставить другие catch -блоки после catch(. ) не имеет смысла. Внутри catch(. ) нельзя напрямую обратиться к объекту-исключению. Но можно перебросить тот же объект, чтобы его поймал другой обработчик: Можно использовать throw в catch -блоках с указанным типом исключения. Но если поместить throw вне блока catch , то программа тут же аварийно завершит работу через вызов std::terminate() . Перебросить исключение можно другим способом: Этот способ обладает дополнительным преимуществом: можно сохранить исключение и перебросить его в другом месте. Однако результат std::current_exception() — это не объект исключения, поэтому его можно использовать только со специализированными функциями. Чтобы избежать лишних копирований, можно ловить исключение по ссылке или константной ссылке: Это особенно полезно, когда мы ловим исключение по базовому типу. Чтобы поймать исключение, нужно его вначале выбросить. Для этого применяется throw. Если throw используется с параметром, то он не перебрасывает исключение, а выбрасывает новое. Параметр может быть любого типа, даже примитивного. Использовать такую конструкцию разрешается в любом месте программы: Вывод: «Поймано исключение типа int, содержащее число –15». Выбрасывать int или другой примитивный тип можно, но это считается дурным тоном. Куда лучше создать специальный тип, который будет использоваться только для исключений. Причём удобно для каждого вида ошибок сделать отдельный класс. Он даже не обязан содержать какие-то данные или методы: отличать исключения друг от друга можно по названию типа. В итоге будет напечатана только фраза: «Найдено отрицательное число», поскольку –15 проверено раньше нуля. Чтобы поймать исключение, тип обработчика должен в точности совпадать с типом исключения. Например, нельзя поймать исключение типа int обработчиком типа unsigned int . Но есть ситуации, в которых типы могут не совпадать. Про одну уже сказано выше: можно ловить исключение по ссылке. Есть ещё одна возможность — ловить исключение по базовому типу. Например, чтобы не писать много catch -блоков, можно сделать все используемые типы исключений наследниками одного. В этом случае рекомендуется принимать исключение по ссылке. Напомню, что тернарная операция ?: позволяет выбрать из двух альтернатив в зависимости от условия: Оператор throw можно использовать внутри тернарной операции в качестве одного из альтернативных значений. Например, так можно реализовать безопасное деление: Это эквивалентно такой записи: Согласитесь, первый вариант лаконичнее. Так можно выбрасывать несколько исключений в одном выражении: Блок try может быть всем телом функции: Тут мы просто опустили фигурные скобки функции. По-другому можно записать так: Есть как минимум два случая возникновения исключений в конструкторе объекта: В первом случае исключение ещё можно поймать внутри тела конструктора и сделать вид, как будто ничего не было. Как в С++ обрабатывать ошибки в конструкторах без исключений? Во втором случае исключение тоже можно поймать, если использовать try-блок в качестве тела конструктора. Однако тут есть особенность: сделать вид, что ничего не было, не получится. Объект всё равно будет считаться недоконструированным: Тут мы увидим оба сообщения: «Знаменатель дроби не может быть нулём» и «Дробь не построена». Если объект недоконструирован, то его деструктор не вызывается. Это логичная, но неочевидная особенность языка. Однако все полностью построенные члены – данные объекта будут корректно удалены: Запустим код и увидим такой вывод: Объект типа A создался и удалился, а объект типа B создался не до конца и поэтому не удалился. Не все исключения в конструкторах можно обработать. Например, нельзя поймать исключения, выброшенные при конструировании глобальных и thread_local объектов, — в этом случае будет вызван std::terminate . В этом разделе примера не будет, потому что исключения в деструкторе — нежелательная практика. Бывает, что язык удаляет объекты вынужденно, например, при поиске обработчика выброшенного исключения. Если во время этого возникнет другое исключение в деструкторе какого-то объекта, то это приведёт к вызову std::terminate . Более того, по умолчанию исключения в деструкторе запрещены и всегда приводят к вызову std::terminate . Выможете разрешить их для конкретного конструктора — об этом я расскажу в следующей части — но нужно много раз подумать, прежде чем сделать это. Поговорка «не пойман — не вор» для исключений не работает. Непойманные исключения приводят к завершению программы через std::terminate . Это нештатная ситуация, но можно предотвратить немедленное завершение, добавив обработчик для std::terminate : Однако не стоит надеяться, что программа после обработки такой неприятной ситуации продолжит работу как ни в чём не бывало. std::terminate — часть завершающего процесса программы. Внутри него доступен только ограниченный набор операций, зависящий от операционной системы. Остаётся только сохранить всё, что можно, и извиниться перед пользователем за неполадку. А затем выйти из программы окончательно вызовом std::abort() . Далеко не всегда есть смысл создавать новый тип исключений, ведь в стандартной библиотеке их и так немало. А если вы всё же создаёте свои исключения, то сделайте их наследниками одного из базовых. Рекомендуется делать все типы исключений прямыми или косвенными наследниками std::exception . Обратим внимание на одну важную вещь. Все описываемые далее классы не содержат никакой магии. Это обычные и очень простые классы, которые вы могли бы реализовать и самостоятельно. Использовать их можно и без throw , однако смысла в этом немного. Их особенность в том, что разработчики договорились использовать эти классы для описания исключений, генерируемых в программе. Например, этот код абсолютно корректен, но совершенно бессмысленен: Разберём основные типы исключений, описанные в стандартной библиотеке C++. Базовый класс всех исключений стандартной библиотеки. Конструктор не принимает параметров. Имеет метод what() , возвращающий описание исключения. Как правило, используются производные классы, переопределяющие метод what() . Исключение типа logic_error выбрасывается, когда нарушены условия, сформулированные на этапе написания программы. Например, мы передали в функцию извлечения квадратного корня отрицательное число или попытались извлечь элемент из пустого списка. Конструктор принимает сообщение в виде std::string , которое будет возвращаться методом what() . Перечислим некоторые производные классы std::logic_error . У всех них похожий интерфейс. Это исключение выбрасывают функции преобразования строки в число, такие как stol , stof , stoul , а также конструктор класса std::bitset : std::runtime_error — ещё один базовый тип для нескольких видов исключений. Он говорит о том, что исключение относится скорее не к предусмотренной ошибке, а к выявленной в процессе выполнения. При этом, если std::logic_error подразумевает конкретную причину ошибки — нарушение конкретного условия, — то std::runtime_error говорит о том, что что-то идёт не так, но первопричина может быть не вполне очевидна. Интерфейс такой же, как и у logic_error : класс принимает описание ошибки в конструкторе и переопределяет метод what() базового класса std::exception . Рассмотрим некоторые важные производные классы: У std::exception есть и другие наследники. Самый важный — std::bad_alloc . Его может выбрасывать операция new. Это исключение — слабое место многих программ и головная боль многих разработчиков, ведь оно может возникать практически везде — в любом месте, где есть динамическая аллокация. То есть при: При обработке bad_alloc нужно соблюдать осторожность и избегать других динамических аллокаций. Возможный вывод: «Место закончилось после вставки 2640 элементов». При аллокациях возможна также ошибка std::bad_array_new_length , производная от bad_alloc . Она возникает при попытке выделить слишком большое, слишком маленькое (меньше, чем задано элементов для инициализации) либо отрицательное количество памяти. Также при аллокации можно запретить new выбрасывать исключение. Для этого пишем (std::nothrow) после new : В случае ошибки операция будет возвращать нулевой указатель. bad_alloc настолько сложно учитывать, что многие даже не пытаются это делать. Мотивация такая: если память закончилась, то всё равно программе делать уже нечего. Лучше поскорей вызвать std::terminate и завершиться. В этой части мы разобрали, как создавать исключения C++, какие они бывают и как с ними работать. Разобрали ключевые слова try , catch и throw . В следующей части запустим бенчмарк, разберём гарантии безопасности, спецификации исключений, а также узнаем, когда нужны исключения, а когда можно обойтись без них. И главное — узнаем, как они работают. Исключения не так просты, как кажутся на первый взгляд. Они нарушают естественный ход программы и кратно увеличивают количество возможных путей исполнения. Но без них ещё сложнее. C++ позволяет выразительно обрабатывать исключения, он аккуратен при удалении всех объектов и освобождении ресурсов. Будьте аккуратны и вы, и тогда всё получится. Каждому исключению — по обработчику. Исключения — это лишь одна из многих возможностей C++. Глубже погрузиться в язык и узнать больше о нём, его экосистеме и принципах программирования поможет курс «Разработчик C++». Следите за новыми постами по любимым темам Подпишитесь на интересующие вас теги, чтобы следить за новыми постами и быть в курсе событий. (C) Dale, 01.02.2011 — 02.02.2011. Язык C сегодня — довольно неоднозначное явление. Ему уже около 40 лет, а он до сих пор в строю. Второго такого долгожителя вряд ли сыщешь — даже Fortran-IV сдал позиции. Этот язык стал родоначальником множества других, включая столь популярные сегодня C++, Java и C#, по при этом не торопится на пенсию — хотя потомки изрядно его потеснили, но так и не вытеснили полностью. Конечно, язык, основы которого закладывались еще в конце 1960-х годов, не может быть безупречным по сегодняшним меркам. Хотя он и развивался все это время, о чем свидетельствует последовательность стандартов языка, но в узких рамках совместимости с предыдущими версиями (не забываем, что главный конек C — это совместимость и переносимость), то есть умеренно. Поэтому у него до сих пор есть ряд очевидных слабостей, которые приходится преодолевать программистам. Сегодня использование C можно было бы рассматривать как архаизм, если бы не одна ниша, в которой ему просто нет равных — программирование микропроцессоров. Сочетание машинной независимости с гибкостью непосредственной работы с аппаратными ресурсами и эффективностью объектного кода делают его незаменимым инструментом разработки firmware, а недостатки преодолеваются различными трюками. Один из таких трюков мы рассмотрели подробно в статье « Сопрограммы в языке программирования C », а сегодня познакомимся поближе с другим. Один из наиболее скользких вопросов программирования на C — это, безусловно, обработка ошибок. Вообще говоря, обработка ошибок — это показатель мастерства и профессиональной зрелости программиста. Плохие программисты пишут программы, которые работают неправильно. Посредственные — пишут программы, которые работают правильно, но в тепличных условиях (оборудование исправно, все файлы на своем месте, данные имеют корректный формат и т.д.). Настоящие мастера пишут программы, которые надежны в любых условиях — если все гладко, они выдают правильный результат, а если по ходу работы возникли проблемы, они либо пытаются бороться с ними по мере возможностей, либо выдают внятную и недвусмысленную диагностику с описанием причин, по которым работа невозможна. Прямо скажем, «чистый» C — плохой помощник разработчику по части обработки ошибок. В самом языке для этого нет никаких средств, а в стандартных библиотеках нет единодушия по вопросу, каким образом функция должна сообщать клиенту об ошибках при выполнении. Иногда функция возвращает код завершения, он же код ошибки, если имеет ненулевое значение; в других случаях часть диапазона значений функций выделена под коды ошибки; и апофеоз — errno. Тут впору вспомнить Дейкстру: «. когда я начинаю анализировать свои собственные мыслительные привычки и привычки моих друзей, я прихожу, нравится мне это или нет, к совершенно иному заключению, а именно: инструменты, которые мы пытаемся использовать, и язык и обозначения, которые мы используем для выражения или записи наших мыслей, являются основным фактором, который определяет, о чем мы вообще можем мыслить и что можем выразить! Анализ влияния, которое язык программирования оказывает на своих пользователей, и признание факта, что к настоящему времени мощь нашего мозга является наиболее скудным ресурсом, совместно дают нам новый набор эталонов для сравнения относительных достоинств различных языков программирования» (из статьи « Смиренный программист »). Язык C — не исключение: вызвав потенциально чреватую ошибками функцию, следует проверить код ее завершения (или errno) и запрограммировать действия в случае ошибки; поскольку эти действия сами могут вызвать ошибку, их результат также следует проверить, и. В результате корректная программа оказывается загроможденной кодом обработки ошибок. Менее прилежные программисты, утомившись, просто бросают это дело, и результат в виде вылетов программы, неожиданных результатов и прочих сюрпризов не заставляет себя ждать. Наследники C, учитывая отрицательный опыт родителя, обзавелись средствами, позволяющими облегчить жизнь программиста при обработке ошибок. В C++ появились исключения, пусть и не слишком изящные, затем в Java и C# эта концепция была доведена до практически приемлемого уровня. Применение механизма исключений позволяет писать код, который решает свою задачу, а в случае, когда продолжение работы невозможно, выбрасывает исключение. В подходящем месте программы размещается перехватчик исключений, который предпринимает соответствующие действия. Структура программы улучшается, т.к. обработка ошибок производится не в том месте, где они возникают, а в том, где это наиболее целесообразно. К счастью, C располагает ценным средством для компенсации своих многочисленных недостатков и слабостей — препроцессором. Инструмент этот довольно опасен, но при умелом использовании дает замечательные результаты. Сегодня мы рассмотрим одно из таких успешных применений макросов — реализацию механизма исключений на языке ANSI C. А для этого мы попробуем решить очень простую, но весьма типичную задачу. Рассмотрим простую задачу. Треугольник задан тремя сторонами a, b, c. Нам нужно определить его площадь. В принципе задача довольно легко решаема: double triangleArea ( double a , double b , double c ) Впрочем, идеальным это решение не назовешь. Функция реализует вычисление площади по формуле Герона, не заботясь о корректности значений переданных ей параметров. Во-первых, отрицательное значение длины стороны треугольника лишено физического смысла. Во-вторых, не каждые три отрезка положительной длины образуют треугольник. Помимо бессмысленного результата, можно получить и более серьезную проблему в виде отрицательного значения под корнем. Самое простое — возложить ответственность за корректность параметров на клиента, вызывающего нашу функцию. Но такое решение сродни заметанию мусора под ковер. К тому же, если функция будет вызываться достаточно часто (а ведь именно для этого и придуманы функции), проверку параметров перед вызовом придется производить во многих местах. И даже это не дает гарантии: достаточно забыть проверить параметры в одном месте программы — и благодатная почва для потенциального сбоя подготовлена. Лучше, конечно, проверить параметры внутри функции, до начала вычислений. double triangleArea ( double a , double b , double c ) Вот только не совсем понятно, что делать, если параметры некорректны. Конечно, можно вернуть нулевое или отрицательное значение вызывающей программе как указание на то, что при выполнении возникла нештатная ситуация: double triangleArea ( double a , double b , double c ) Как вариант, можно вообще прекратить выполнение программы при ошибке, чтобы заведомо избежать нежелательных последствий: #include double triangleArea ( double a , double b , double c ) Однако такой вариант очень ограничивает использование нашей функции. Например, если значения вводятся с клавиатуры, велика вероятность опечатки; в этом случае логично было бы вывести предупреждение и предложение повторить ввод. Останов по ошибке исключает такую возможность, вынуждая перезапустить программу с самого начала. Это одна из причин, по которым язык C сегодня многие не воспринимают всерьез, считая, что на нем невозможно писать изящные программы. Зачастую такое мнение оказывается ошибочным: C может очень многое, гораздо больше, чем кажется на первый взгляд. Нужно лишь воспользоваться одним из многочисленных средств для расширения его возможностей. Одним из таких средств является CException. Основы CException Пользоваться CException очень просто. Его основу образуют три макроса: Try, Catch и Throw, а также тип CEXCEPTION_T. CEXCEPTION_T Для того, чтобы обработчик ошибки мог выяснить, какая именно ошибка произошла, каждый тип ошибки должен иметь свой идентификатор. Тип этого идентификатора должен быть CEXCEPTION_T. По умолчанию он соответствует unsigned int. Этот макрос начинает защищенный блок. Синтаксис его подобен синтаксису обычного if: за ним следует либо единичный оператор, либо блок. За ним обязательно должен следовать блок Catch (если вы забудете об этом, получите ошибку компиляции). Блоки Try могут быть вложены друг в друга. При необходимости можно повторно выбросить исключение, чтобы обработать его во внешнем блоке Try. Этот макрос начинает блок обработки ошибок. Он выполняется лишь в том случае, если в блоке Try возникло исключение. Исключение может возникнуть как непосредственно в коде внутри блока Try, так и в функции, вызываемой в блоке Try на любом уровне вложенности. Catch получает идентификатор исключения типа CEXCEPTION_T, который вы можете использовать при обработке ошибки. Если в процессе обработки возникнет исключение, оно будет обработано внешним блоком Try. Используется для того, чтобы выбросить исключение. Принимает единственный аргумент — идентификатор исключения, который будет передан Catch в качестве причины ошибки. Как уже упоминалось ранее, в процессе обработки ошибки можно опять выбрасывать исключение, в том числе и повторно то же самое, которое обрабатывается в данный момент. Паттерн обработки ошибок . потенциально_опасная_функция() Теперь мы можем сосредоточить весь код обработки ошибок в единственном месте — блоке Catch, не загромождая основной поток выполнения программы. Окончательный вариант программы enum ERRORCODE_T Интерфейс функции вычисления треугольника остается неизменным: extern double triangleArea ( double a , double b , double c ) ; А вот в тело функции внесем изменения с учетом того, что узнали про обработку исключений: #include double triangleArea ( double a , double b , double c ) Ну и в завершение — главная программа: #include int main ( int argc , char * argv [ ] ) Try case BAD_TRIANGLE : default : system ( "PAUSE" ) ; Улучшение структуры кода сразу бросается в глаза: функция сообщает о возникновении ошибки, а инфраструктура CException передает управление обработчику ошибок без участия программиста. Больше нет необходимости загромождать текст программы условными операторами (не забываем, что по-хорошему программу нужно еще тестировать, а наличие каждого ветвления усложняет эту процедуру). Заключение Конечно, CException — не панацея, и его применение не решит всех проблем. Например, CEXCEPTION_T — это всего лишь целочисленная константа, и с ее помощью невозможно сообщить обработчику все детали об ошибке. Кроме того, обработчик Catch в отличие от настоящих обработчиков, встроенных в современные языки программирования, перехватывает все исключения, поэтому вполне возможно, что некоторые из них, не предназначенные данному обработчику, придется выбросить повторно. Но все же плюсов от использования CException гораздо больше чем минусов, и программы на C, написанные с использованием механизма исключений, окажутся проще и надежнее своих старорежимных собратьев. Область применения: управляемое приложение, мобильное приложение, обычное приложение. 1. В общем случае не рекомендуется перехватывать исключения. В частности не нужно перехватывать исключения только ради выдачи сообщения об ошибке. Необработанное исключение в любом случае будет выдано пользователю в виде сообщения об ошибке (а также будет автоматически записано в журнал регистрации для администратора и отправлено в сервис регистрации ошибок). 2. Тем не менее, необходимость перехвата исключений в коде все же возникает. Например, для того чтобы уточнить текст ошибки, дополнив его прикладной, понятной конечному пользователю, информацией. Однако при этом необходимо фиксировать причину ошибки в журнале регистрации для того, чтобы системный администратор имел возможность выполнить диагностику проблемы и при необходимости передать информацию об ошибке в службу технической поддержки. При этом рекомендуется записывать в журнал регистрации подробное представление исключения, а краткое представление добавлять в текст сообщения пользователю. 3. Частные случаи некорректного использования и перехвата исключений. Область применения (уточнение): управляемое приложение, обычное приложение. 3.1. Если имеется некоторая серверная бизнес-логика, которая вызывается с клиента при интерактивной работе пользователя: &НаСервере то неправильно маскировать от пользователя и администратора исходную проблему: // на клиенте Правильно записывать в журнал регистрации подробное представление исключения, а краткое представление добавлять в текст сообщения пользователю: и тогда на клиенте: Попытка Не следует использовать функцию ОписаниеОшибки , т.к. она неинформативна для разработчика, потому что не возвращает стек в тексте ошибки. 3.2. Не следует анализировать текст исключений с целью интерпретации причины ошибки. Текст исключения может меняться в зависимости от локализации. В условиях отсутствия штатных средств (например, типизированных исключений), следует выдавать пользователю тексты исключений «как есть». Для понятности, его можно дополнить пояснением возможных причин. Попытка В тех случаях, когда анализ типов исключений критически важен для корректной работы бизнес-логики, следует отказаться от исключений и использовать коды ошибок (коды возврата). При этом недопустимо использовать числовые коды ошибок, т.к. код становится нечитаемым: КодОшибки = ЗагрузитьФайлИзИнтернета(. ); правильно применять строковые литералы (например, "Успешно", "НетМестаНаДиске", "Отменено" и т.п.): РезультатЗагрузки = ЗагрузитьФайлИзИнтернета(. ); Строковые литералы кодов ошибок не локализуются. Исключение составляют случаи работы с веб-сервисами и другими внешними системами, где коды ошибок не доступны, а результат работы транслируется в вызывающий код прикладной конфигурации в виде исключений. 3.3. Если имеется некоторая клиентская бизнес-логика (код выполняется полностью на клиенте): &НаКлиенте то рекомендуется делать дополнительный серверный вызов для протоколирования неудачного результата операции в журнал регистрации: Попытка 3.4. Недопустимо перехватывать любые исключения, бесследно для системного администратора: Попытка Как правило, подобная конструкция скрывает реальную проблему, которую впоследствии невозможно диагностировать. Попытка 3.5. Неприемлемо в событиях ПриЗаписи , ПередЗаписью , ПередУдалением , ОбработкаПроведения , ОбработкаПроверкиЗаполнения и т.п. вызывать исключения для выдачи останавливающих предупреждений, которые должен отработать пользователь. Вместо этого следует устанавливать параметр Отказ в значение Истина и выводить сообщения пользователю. Тем самым пользователь увидит все блокирующие причины остановки сразу, а не по очереди. Но если пользователь не может отработать останавливающее предупреждение в рамках выполняемой операции или ошибочная ситуация носит исключительный характер и делает бессмысленными все остальные проверки, которые выводят другие останавливающие предупреждения, то следует вызывать исключение. Процедура ПередЗаписью(Отказ) Подробнее см. пп. 1.1 и 1.3 стандарта Информирование пользователя. 3.6. Недопустимо делать проверки наличия у объекта реквизитов, методов, макетов и т.п., используя для этого исключения, т.к. это может привести к сложно диагностируемым ошибкам, а также затрудняет отладку в режиме «Останавливаться по ошибке». Попытка МакетКомпонентыОбмена = КонтекстЭДОСервер.Метаданные().Макеты.Найти("КомпонентаОбмена"); 3.7. Порядок обработки исключений при использовании транзакций описан в стандарте Транзакции: правила использования. 3.8. Неправильно использовать исключения для приведения значения к типу. Для таких операций необходимо использовать возможности объекта ОписаниеТипов . Попытка ОписаниеТипа = Новый ОписаниеТипов("Число");double z
Error! The End.
Division by zero! The End.
throw std::string;
void print(int argument) noexcept;
Создание объекта исключения
Обработка и генерация разных типов исключений
#include double divide(int a, int b) < if(!b) // если b == 0 < throw 0; >if(b > a) < throw "The first number is greater than the second one"; >return a / b; > void test(int a, int b) < try < double result
throw 0;
throw "The first number is greater than the second one";
test(100, 20); // 5
test(100, 0); // Error code: 0
test(100, 1000); // The first number is greater than the second one
5 Error code: 0 The first number is greater than the second one
void test(int a, int b) < try < double result
throw 0;
try-catch и деструкторы
#include class Person < public: Person(std::string name) :name< name > < std::cout ~Person() < std::cout void print() < throw "Print Error"; >private: std::string name; >; int main() < try < Person tom< "Tom" >; tom.print(); // Здесь генерируется ошибка > catch (const char* error) < std::cerr >
Person Tom created Person Tom deleted Print Error
Исключения в C++: типы, синтаксис и обработка
Инструмент программирования для исключительных ситуаций
Исключения: панацея или нет
Синтаксис исключений в C++
Ловим исключения
void SomeFunction() < DoSomething0(); try < SomeClass var; DoSomething1(); DoSomething2(); // ещё код cout catch(ExceptionType e) < std::cout std::cout
Ловим исключения нескольких типов
void SomeFunction() < DoSomething0(); try < DoSomething1(); DoSomething2(); // ещё код >catch(ExceptionType1 e) < std::cout catch(ExceptionType2 e) < std::cout // ещё код > Ловим все исключения
void SomeFunction() < DoSomething0(); try < DoSomething1(); DoSomething2(); // ещё код >catch(. ) < std::cout // ещё код > Перебрасываем исключение
void SomeFunction() < DoSomething0(); try < DoSomething1(); DoSomething2(); // ещё код >catch(. ) < std::cout // ещё код > std::rethrow_exception(std::current_exception()) Принимаем исключение по ссылке
void SomeFunction() < DoSomething0(); try < DoSomething1(); DoSomething2(); // ещё код >catch(ExceptionType& e) < std::cout catch(const OtherExceptionType& e) < std::cout > Выбрасываем исключения
void ThrowIfNegative(int x) < if (x < 0) < // выбрасываем исключение типа int throw x; >> int main() < try < ThrowIfNegative(10); ThrowIfNegative(-15); ThrowIfNegative(0); cout // ловим выброшенное исключение catch(int x) < cout > Создаём типы для исключений
class IsZeroException<>; struct IsNegativeException<>; void ThrowIfNegative(int x) < if (x < 0) < // Выбрасывается не тип, а объект. // Не забываем скобки, чтобы создать объект заданного типа: throw IsNegativeException(); >> void ThrowIfZero(int x) < if (x == 0) < throw IsZeroException(); >> void ThrowIfNegativeOrZero(int x) < ThrowIfNegative(x); ThrowIfZero(x); >int main() < try < ThrowIfNegativeOrZero(10); ThrowIfNegativeOrZero(-15); ThrowIfNegativeOrZero(0); >catch(IsNegativeException x) < cout catch(IsZeroException x) < cout > Ловим исключение по базовому типу
class NumericException < public: virtual std::string_view what() const = 0; >// Класс — наследник NumericException. class IsZeroException : public NumericException < public: std::string_view what() const override < return "Обнаружен ноль"; >> // Ещё один наследник NumericException. class IsNegativeException : public NumericException < public: std::string_view what() const override < return "Обнаружено отрицательное число"; >> void ThrowIfNegative(int x) < if (x < 0) < // Выбрасывается не тип, а объект. // Не забываем скобки, чтобы создать объект заданного типа: throw IsNegativeException(); >> void ThrowIfZero(int x) < if (x == 0) < throw IsZeroException(); >> void ThrowIfNegativeOrZero(int x) < ThrowIfNegative(x); ThrowIfZero(x); >int main() < try < ThrowIfNegativeOrZero(10); ThrowIfNegativeOrZero(-15); ThrowIfNegativeOrZero(0); >// Принимаем исключение базового типа по константной ссылке (&): catch(const NumericException& e) < std::cout > Выбрасываем исключение в тернарной операции ?:
std::cout = 18 ? "Проходите" : "Извините, вход в бар с 18 лет") int result = y != 0 ? x / y : throw IsZeroException(); int result; if (y != 0) < result = x / y; >else
// Вычислим корень отношения чисел: int result = y == 0 ? throw IsZeroException() : x / y < 0 ? throw IsNegativeException() : sqrt(x / y);Вся функция — try-блок
int SomeFunction(int x) try < return DoSomething(x); >catch(ExceptionType e) < std::cout int SomeFunction(int x) < try < return DoSomething(x); >catch(ExceptionType e) < std::cout > Исключения в конструкторе
class IsZeroException<>; // Функция выбросит исключение типа IsZeroException // если аргумент равен нулю. void ThrowIf0(int x) < if (x == 0) < throw IsZeroException(); >> // Класс содержит только одно число. // Он выбрасывает исключение в конструкторе, если число нулевое. class NotNullInt < public: NotNullInt(int x) : x_(x) < ThrowIf0(x_); >private: int x_; > class Ratio < public: // Инициализаторы пишем после try: Ratio(int x, int y) try : x_(x), y_(y) < >catch(IsZeroException e) < std::cout private: int x_; NotNullInt y_; >; int main() < Ratio(10, 15); try < Ratio(15, 0); >catch(. ) < std::cout > #include class A < public: A() < std::cout ~A() < std::cout private: > class B < public: B() < std::cout ~B() < // Этой надписи мы не увидим: std::cout private: A a; >; int main() < try < B b; >catch (. ) < >> A constructed B constructed A destructed Исключения в деструкторе
Обрабатываем непойманные исключения
int main() < // Запишем обработчик в переменную terminate_handler auto terminate_handler = []() < auto e_ptr = std::current_exception(); if (e_ptr) < try < // Перебросим исключение: std::rethrow_exception(e_ptr); >catch (const SomeType& e) < std::cerr catch (. ) < std::cerr > else < std::cerr // Всё равно завершим программу. std::abort(); >; // Установим обработчик для функции terminate std::set_terminate(terminate_handler); // ….. > Базовые исключения стандартной библиотеки
#include #include int main() < // Используем std::runtime_error вместо std::string. // Но зачем? std::runtime_error err("Буря мглою небо кроет"); std::cout
std::exception
std::logic_error : public std::exception
// класс копилка class Moneybox < public: void WithdrawCoin() < if (coins_ == 0) < throw std::logic_error("В копилке нет денег"); >--coins_; > void PutCoin() < ++coins_; >private: int coins_ = 0; >
double GetSqrt(double x) < return x >= 0 ? sqrt(x) : throw std::invalid_argument("Попытка извлечь квадратный корень из отрицательного числа"); > try < int f = std::stoi("abracadabra"); >catch (std::invalid_argument& ex)
std::runtime_error : public std::exception
class CommandLineParsingError : public std::runtime_error < public: // этой строкой импортируем конструктор из базового класса: using runtime_error::runtime_error; >; class ZeroDenominatorError : public std::runtime_error < public: // используем готовое сообщение: ZeroDenominatorError() : std::runtime_error("Знаменатель не может быть нулём") < >>
std::bad_alloc : public std::exception
#include #include #include #include int main() < std::vector vec; try < while (true) < vec.push_back(std::string(10000000, 'a')); >> catch (const std::bad_alloc& e) < std::cout > int main() < int* m = new (std::nothrow) int [0xFFFFFFFFFFFFFFULL]; std::cout Заключение

Как вызвать исключение c
{
double p = ( a + b + c ) / 2.0 ;
double result = sqrt ( p * ( p - a ) * ( p - b ) * ( p - c ) ) ;
return result ;
}
( a
#include
( a
Располагая описанными выше примитивами, можно написать надежную программу по следующему образцу:
// Защищенный блок
Try
.
потенциально_опасный код
if (возникла_ошибка)
Throw(e1); // e1 - код конкретной ошибки
.
вызов_потенциально_опасной_функции()
.
>
// Сюда мы попадаем только в случае возникновения ошибки
Catch(e) // здесь e - код возникшей ошибки
.
код_обработки_ошибки
.
>
// Конец защищенного блока
.
.
if (возникла_ошибка)
Throw(e2); // e2 - код конкретной ошибки
.
>
Сначала определим коды возможных ошибок, чтобы наша программа не пестрила «магическими числами».
{
NEGATIVE_SIDE ,
BAD_TRIANGLE
} ;
#include
#include "CException.h"
#include "ErrorCode.h"
#include "TriangleArea.h"
#include
#include "TriangleArea.h"
#include "CException.h"
#include "ErrorCode.h"
{
CEXCEPTION_T e ;
double a , b , c ;
a = 10.0 ;
b = 2.0 ;
c = 2.0 ;
printf ( "a=%f b=%f c=%f \n " , a , b , c ) ;
{
double area = triangleArea ( a , b , c ) ;
printf ( "area=%f \n " , area ) ;
}
Catch ( e )
{
switch ( e )
{
case NEGATIVE_SIDE :
printf ( "One of triangle sides is negative. \n " ) ;
break ;
printf ( "Triangle cannot be made of these sides. \n " ) ;
break ;
printf ( "Unknown error: %d \n " , e ) ;
}
}
return 0 ;
}Перехват исключений в коде
Процедура ВыполнитьОперацию()
// код, приводящий к вызову исключения
.
КонецПроцедуры
Попытка
ВыполнитьОперацию();
Исключение
ПоказатьПредупреждение(,НСтр("ru = 'Операция не может быть выполнена.'"));
КонецПопытки;
ВыполнитьОперацию();
Исключение
ТекстСообщения = КраткоеПредставлениеОшибки(ИнформацияОбОшибке());
ПоказатьПредупреждение(,НСтр("ru = 'Операция не может быть выполнена по причине:'") + Символы.ПС + ТекстСообщения);
КонецПопытки;
Например:
ЗагрузитьФайлИзИнтернета(. );
Исключение
ТекстСообщения = КраткоеПредставлениеОшибки(ИнформацияОбОшибке());
ТекстСообщения = НСтр("ru = 'Не удалось загрузить файл:'") + Символы.ПС + ТекстСообщения + Символы.ПС + НСтр("ru = 'Возможные причины:
• Нет подключения к Интернету;
• На веб-узле возникли неполадки;
• Брандмауэр или другое промежуточное ПО (антивирусы и т.п.) блокируют попытки программы подключиться к Интернету;
• Подключение к Интернету выполняется через прокси-сервер, но его параметры не заданы в программе.'");
ПоказатьПредупреждение(,ТекстСообщения);
КонецПопытки;
Если КодОшибки = 12345 Тогда
.
ИначеЕсли .
Если РезультатЗагрузки = "Успешно" Тогда
.
ИначеЕсли .
Процедура СоздатьФайлНаДиске()
// код, приводящий к вызову исключения
.
КонецПроцедуры
// клиентский код, приводящий к вызову исключения
СоздатьФайлНаДиске();
Исключение
ТекстСообщения = КраткоеПредставлениеОшибки(ИнформацияОбОшибке());
ПоказатьПредупреждение(,НСтр("ru = 'Операция не может быть выполнена по причине:'") + Символы.ПС + ТекстСообщения);
ЗаписатьОшибкуРаботыСФайлами(ОбработкаОшибок.ПодробноеПредставлениеОшибки(ИнформацияОбОшибке())));
КонецПопытки;
// код, приводящий к вызову исключения
.
Исключение // перехват любых исключений
КонецПопытки;
Правильно:
// код, приводящий к вызову исключения
.
Исключение
// Пояснение причин перехвата всех исключений "незаметно" от пользователя.
// .
// И запись события в журнал регистрации для системного администратора.
ЗаписьЖурналаРегистрации(НСтр("ru = 'Выполнение операции'"),
УровеньЖурналаРегистрации.Ошибка.
ОбработкаОшибок.ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
КонецПопытки;
Если Не ЗарегистрироватьИзмененияНаУзлахПлановОбмена() Тогда
ВызватьИсключение НСтр("ru = 'Не удалось зарегистрировать изменения на узлах планов обмена. Обратитесь к администратору.'");
КонецЕсли;
.
КонецПроцедуры
Вместо перехвата исключений в этом случае рекомендуется:
КонтекстЭДОСервер.ПолучитьМакет("КомпонентаОбмена");
ПутьВК = КонтекстЭДОСервер.ПутьКОбъекту + ".Макет.КомпонентаОбмена";
Исключение
КонецПопытки;
Если МакетКомпонентыОбмена <> Неопределено Тогда
ПутьКМакету = КомпонентаОбмена.ПолноеИмя()
КонецЕсли;
КоличествоДнейРазрешения = Число(Значение);
Исключение
КоличествоДнейРазрешения = 0; // значение по умолчанию
КонецПопытки;
КоличествоДнейРазрешения = ОписаниеТипа.ПривестиЗначение(Значение);См. также