Мы уже рассматривали базовые принципы защитного программирования и разобрались с тем, как можно организовать передачу и обработку ошибок на основе их числовых кодов. Если вы еще не ознакомились с указанной вводной частью, то рекомендую ее хотя бы пролистать, поскольку в этой заметке я буду периодически делать на нее отсылки.
Числовые коды — это хорошо, и в некоторых случаях у вас не будет других вариантов, кроме как пользоваться ими. Однако механизм исключений во многом оказывается более удобным и гибким. Давайте разберемся, почему это так.
- Почему исключения?
- Вариант 1. Коды ошибок. Опять
- Вариант 2. Иерархия исключений
- Вариант 3. Текстовые сообщения
- Коды ошибок, иерархия или текстовые сообщения?
- Коротко об освобождении ресурсов
- Заключение
В любой программе, сложнее чем
“Hello, world”, неизбежно будут происходить ошибки и различные сбои. Поэтому если вы хотите писать надежный и стабильный код, то обязаны заботиться обо всем этом. Конечно, в большинстве случаев нет смысла становиться параноиком и проверять абсолютно все. С другой стороны, во многом это зависит от области применения приложения, которое вы разрабатываете. Если это система контроля банковских переводов или управления полетом спутника, то единственная ошибка в коде может обойтись очень дорого. В этом случае вам на помощь приходит «защитное программирование». Одно из лучших описаний этой методики, на мой взгляд, приводится в следующих книгах:
- Ремесло программиста. Практика написания хорошего кода;
- Совершенный код.
Суть защитного программирования заключается в аккуратной и последовательной обработке всех возможных ошибок и исключений. О том, какими способами это можно сделать, мы и поговорим.
Примеры кода я буду приводить на C++, однако это не должно стать для вас серьезной помехой, если вы пишите на Java, C# или каком-нибудь другом языке со схожим синтаксисом, поскольку многие техники обработки ошибок являются универсальными.
- Коды ошибок
- Двойная ответственность
- Указатель для ошибки
- Вот это поворот
- Кто там?
- Чуть выше
- Заключение
Что такое исключение? Это ситуация, которая не предусмотрена стандартным поведением программы. Например, попытка доступа к элементу в классе Vector (который мы разбирали в статье про
классы
), который не существует. То есть происходит выход за пределы вектора. В данном случае можно воспользоваться исключениями, чтобы прервать выполнение программы. Это необходимо потому, что
- Как правило в таких случаях, автор класса
Vector
не знает, как пользователь захочет использовать его класс, а также не знает в какой программе этот класс будет использоваться.
- Пользователь класса
Vector
не может всегда контролировать правильность работы этого класса, поэтому ему нужно сообщить о том, что что-то пошло не так.
Для разрешения таких ситуация в C++ можно использовать технику исключений.
Рассмотрим, как написать вызов исключения в случае попытки доступа к элементу по индексу, который не существует в классе Vector.
Здесь применяется исключение
out_of_range. Данное исключение определено в заголовочном файле.
Оператор
throw
передаёт контроль обработчику для исключений типа
out_of_range
в некоторой функции, которая прямо или косвенно вызывает
Vector::operator. Для того, чтобы обработать исключения необходимо воспользоваться блоком операторов
try catch.
Добавлено 2 сентября 2021 в 00:28
В предыдущем уроке об обработке ошибок мы говорили о способах использования assert(), cerr() и exit() для обработки ошибок. Однако тогда мы затронули еще одну тему, которую сейчас и рассмотрим: исключения.
Когда коды возврата не работают
При написании кода, который можно использовать повторно, обработка ошибок обязательна. Один из наиболее распространенных способов обработки потенциальных ошибок – использование кодов возврата. Например:
Эта функция возвращает индекс первого символа в строке, совпадающего с ch. Если символ не найден, функция возвращает -1 в качестве индикатора ошибки.
Основное достоинство этого подхода в том, что он чрезвычайно прост. Однако использование кодов возврата имеет ряд недостатков, которые могут быстро стать очевидными при использовании в нетривиальных случаях.
Во-первых, возвращаемые значения могут быть неочевидными – если функция возвращает -1, пытается ли она указать на ошибку или это действительно допустимое возвращаемое значение? Часто это трудно сказать, не вникая в суть функции.
Во-вторых, функции могут возвращать только одно значение, но что происходит, когда вам нужно вернуть и результат функции, и код ошибки? Рассмотрим следующую функцию:
Эта функция отчаянно нуждается в обработке ошибок, потому что она даст сбой, если пользователь передаст 0 для параметра y. Однако она также должна возвращать результат x/y. Как можно сделать и то, и другое? Наиболее распространенный ответ заключается в том, что либо результат, либо код ошибки должны быть переданы обратно в качестве ссылочного параметра, что делает код более уродливым и менее удобным в использовании. Например:
В-третьих, в последовательностях кода, где многие вещи могут пойти не так, коды ошибок необходимо проверять постоянно. Рассмотрим следующий фрагмент кода, который включает в себя анализ текстового файла на предмет значений, которые должны быть там:
Мы еще не рассмотрели доступ к файлам, поэтому не беспокойтесь, если не понимаете, как это работает – просто обратите внимание на тот факт, что каждый вызов требует проверки на ошибку и возврата в вызывающую функцию. А теперь представьте, что у вас двадцать параметров разных типов – по сути, вы двадцать раз проверяете на наличие ошибки и возвращаете ERROR_READING_VALUE! Вся эта проверка на ошибки и возвращение значений значительно затрудняют определение того, что функция пытается сделать на самом деле.
В-четвертых, коды возврата плохо сочетаются с конструкторами. Что произойдет, если вы создаете объект, и что-то пойдет не так внутри конструктора? Конструкторы не имеют возвращаемого типа для передачи индикатора состояния, а возвращать его через ссылочный параметр не очень красиво и требует явной проверки. Более того, даже если вы это сделаете, объект всё равно будет создан, поэтому затем его нужно будет обработать или утилизировать.
Наконец, когда вызывающему возвращается код ошибки, вызывающая функция не всегда может быть снабжена обработкой ошибок. Если вызывающая функция не хочет обрабатывать ошибку, она должна либо проигнорировать ее (в этом случае она будет потеряна навсегда), либо вернуть ошибку функции, следующей в стеке (той, которая ее вызвала). Это может быть привести к бардаку и ко многим из тех же проблем, о которых говорилось выше.
Подводя итог, основная проблема с кодами возврата заключается в том, что код обработки ошибок в конечном итоге замысловато связан с обычным управлением порядком выполнения кода. Это, в свою очередь, ограничивает как структуру кода, так и приемлемые способы обработки ошибок.
Исключения
Обработка исключений обеспечивает механизм, позволяющий отделить обработку ошибок или других исключительных обстоятельств от обычного управления порядком выполнения вашего кода. Это дает больше свободы в том, когда и как обрабатывать ошибки в какой-либо конкретной ситуации, что облегчает многие (если не все) сложности, вызываемые кодами возврата.
В следующем уроке мы рассмотрим, как исключения работают в C++.
Теги
C++ / CppException / ИсключениеLearnCppДля начинающихОбработка ошибокОбучениеПрограммирование
Добавлено 30 мая 2021 в 21:14
В уроке «7. 14 – Распространенные семантические ошибки при программировании на C++» мы рассмотрели многие типы распространенных семантических ошибок, с которыми сталкиваются начинающие программисты при работе с языком C++. Если ошибка является результатом неправильного использования языковой функции или логической ошибки, исправить ее можно просто.
Но большинство ошибок в программе возникает не в результате непреднамеренного неправильного использования языковых функций – скорее, большинство ошибок возникает из-за ошибочных предположений, сделанных программистом, и/или из-за отсутствия надлежащего обнаружения/обработки ошибок.
Например, в функции, предназначенной для поиска оценки учащегося, вы могли предположить, что:
- просматриваемый студент будет существовать;
- имена всех студентов будут уникальными;
- в предмете используется числовая оценка (вместо «зачет/незачет»).
Что, если какое-либо из этих предположений неверно? Если программист не предвидел этих случаев, программа при возникновении таких случаев, скорее всего, завершится со сбоем (обычно в какой-то момент в будущем, через долгое время после того, как функция была написана).
Есть три ключевых места, где обычно возникают ошибки предположений:
- Когда функция возвращает значение, программист мог предположить, что вызов функции будет успешным, хотя это не так.
- Когда программа получает входные данные (либо от пользователя, либо из файла), программист мог предположить, что ввод будет в правильном формате и семантически корректен, хотя это не так.
- Когда функция была вызвана, программист мог предположить, что параметры будут семантически допустимыми, хотя это не так.
Многие начинающие программисты пишут код, а затем проверяют только счастливый путь: только те случаи, когда ошибок нет. Но вы также должны планировать и проверять печальные пути, на которых что-то может пойти и пойдет не так. В уроке «3. 10 – Поиск проблем до того, как они станут проблемами», мы определили защитное программирование как попытку предвидеть все способы неправильного использования программного обеспечения конечными пользователями или разработчиками (либо самим программистом, либо другими). Как только вы ожидаете (или обнаруживаете) какое-то неправильное использование, следующее, что вам нужно сделать, – это обработать его.
В этом уроке мы поговорим о стратегиях обработки ошибок (что делать, если что-то пойдет не так) внутри функции. В следующих уроках мы поговорим о проверке ввода данных пользователем, а затем представим полезный инструмент, помогающий документировать и проверять предположения.
Обработка ошибок в функциях
Функции могут давать сбой по любому количеству причин – вызывающий мог передать аргумент с недопустимым значением, или что-то может дать сбой в теле функции. Например, функция, открывающая файл для чтения, может не работать, если файл не может быть найден.
Когда это произойдет, в вашем распоряжении будет несколько вариантов. Лучшего способа справиться с ошибкой нет – это на самом деле зависит от характера проблемы и от того, можно ли устранить проблему или нет.
Можно использовать 4 основные стратегии:
- обработать ошибку в функции;
- передать ошибку вызывающему, чтобы он разобрался с ней;
- остановить программу;
- выбросить исключение.
Обработка ошибки в функции
Если возможно, наилучшей стратегией является восстановление после ошибки в той же функции, в которой возникла ошибка, так, чтобы ошибку можно было локализовать и исправить, не влияя на какой-либо код вне функции. Здесь есть два варианта: повторять попытки до успешного завершения или отменить выполняемую операцию.
Если ошибка возникла из-за чего-то, не зависящего от программы, программа может повторять попытку, пока не будет достигнут успех. Например, если программе требуется подключение к Интернету, и пользователь потерял соединение, программа может отобразить предупреждение, а затем использовать цикл для периодической повторной проверки подключения к Интернету. В качестве альтернативы, если пользователь ввел недопустимые входные данные, программа может попросить пользователя повторить попытку и выполнять этот цикл до тех пор, пока пользователь не введет корректные входные данные. Мы покажем примеры обработки недопустимого ввода и использования циклов для повторных попыток в следующем уроке (7. 16 – std::cin и обработка недопустимого ввода).
Альтернативная стратегия – просто игнорировать ошибку и/или отменить операцию. Например:
В приведенном выше примере, если пользователь ввел недопустимое значение для y, мы просто игнорируем запрос на печать результата операции деления. Основная проблема при этом заключается в том, что у вызывающей функции или у пользователя нет возможности определить, что что-то пошло не так. В таком случае может оказаться полезным напечатать сообщение об ошибке:
Однако если вызывающая функция ожидает, что вызываемая функция выдаст возвращаемое значение или какой-либо полезный побочный эффект, тогда просто игнорирование ошибки может быть недопустимым вариантом.
Передача ошибок вызывающей функции
Во многих случаях обработать ошибку с помощью функции, которая ее обнаруживает, невозможно. Например, рассмотрим следующую функцию:
Если y равно 0, что нам делать? Мы не можем просто пропустить логику программы, потому что функция должна возвращать какое-то значение. Мы не должны просить пользователя ввести новое значение для y, потому что это функция вычисления, и введение в нее процедур ввода может быть или не быть подходящим для программы, вызывающей эту функцию.
В таких случаях лучшим вариантом может быть передача ошибки обратно вызывающей функции в надежде, что вызывающая сторона сможет с ней справиться.
Как мы можем это сделать?
Если функция имеет тип возвращаемого значения void, его можно изменить, чтобы она возвращала логическое значение, указывающее на успех или на неудачу. Например, вместо:
Мы можем сделать так:
Таким образом, вызывающий может проверить возвращаемое значение, чтобы узнать, не завершилась ли функция по какой-либо причине неудачей.
Если функция возвращает обычное значение, всё немного сложнее. В некоторых случаях полный диапазон возвращаемых значений не используется. В таких случаях, чтобы указать на ошибку, мы можем использовать возвращаемое значение, которое иначе было бы невозможно. Например, рассмотрим следующую функцию:
Обратное к некоторому числу x определяется как 1/x, а число, умноженное на обратное, равно 1.
Однако что произойдет, если пользователь вызовет эту функцию как reciprocal(0. 0)? Мы получаем ошибку деления на ноль и сбой программы, поэтому очевидно, что мы должны защититься от этого случая. Но эта функция должна возвращать значение doube, так какое же значение мы должны вернуть? Оказывается, эта функция никогда не выдаст 0. 0 как допустимый результат, поэтому мы можем вернуть 0. 0, чтобы указать на случай ошибки.
Однако если требуется полный диапазон возвращаемых значений, то использование возвращаемого значения для указания ошибки будет невозможно (поскольку вызывающий не сможет определить, является ли возвращаемое значение допустимым значением или значением ошибки). В таком случае выходной параметр (рассмотренный в уроке «11. 3 – Передача аргументов по ссылке») может быть жизнеспособным вариантом.
Фатальные ошибки
Если ошибка настолько серьезна, что программа не может продолжать работать правильно, она называется неисправимой ошибкой (или фатальной ошибкой). В таких случаях лучше всего завершить программу. Если ваш код находится в main() или в функции, вызываемой непосредственно из main(), лучше всего позволить main() вернуть ненулевой код состояния. Однако если вы погрузились в какую-то глубоко вложенную подфункцию, передать ошибку обратно в main() может быть неудобно или невозможно. В таком случае можно использовать инструкцию остановки (например, std::exit()).
Поскольку возврат ошибки из функции обратно вызывающей функции сложен (и множество различных способов сделать это приводит к несогласованности, а несогласованность ведет к ошибкам), C++ предлагает совершенно отдельный способ передачи ошибок обратно вызывающей стороне: исключения.
Основная идея состоит в том, что при возникновении ошибки «выбрасывается» исключение. Если текущая функция не «улавливает» ошибку, то уловить ошибку есть шанс у вызывающей функции. Если вызывающая функция не обнаруживает ошибку, то обнаружить ошибку есть шанс у функции, вызвавшей вызывающую функцию. Ошибка постепенно перемещается вверх по стеку вызовов до тех пор, пока она не будет обнаружена и обработана (в этот момент выполнение продолжается в обычном режиме), или пока main() не сможет обработать ошибку (в этот момент программа завершится с ошибкой исключения).
Мы рассмотрим обработку исключений в главе 20 этой серии обучающих статей.
C++ / CppException / ИсключениеLearnCppstd::exit()Для начинающихОбнаружение ошибокОбработка ошибокОбучениеПрограммирование
Добавлено 4 сентября 2021 в 10:45
В предыдущем уроке о необходимости исключений мы говорили о том, как использование кодов возврата приводит к смешиванию порядка выполнения программы и порядка обработки ошибок, ограничивая и то и другое. Исключения в C++ реализуются с использованием трех ключевых слов, работающих вместе: throw, try и catch.
Выбрасывание исключений
В реальной жизни, чтобы отметить, что произошли определенные события, мы постоянно используем сигналы. Например, во время американского футбола, если игрок совершил фол, судья бросает флаг на землю и дает свисток об остановке игры. Затем назначается и исполняется наказание. Как только наказание снято, игра, как правило, возобновляется в обычном режиме.
В C++ для сигнализации о том, что произошло исключение или ошибка (пример с выбрасыванием флага штрафа) используется инструкция throw. Сигнализация о возникновении исключения также обычно называется генерацией или выбрасыванием исключения.
Чтобы использовать инструкцию throw, просто используйте ключевое слово throw, за которым следует значение любого типа данных, который вы хотите использовать, чтобы сигнализировать о возникновении ошибки. Обычно это значение будет кодом ошибки, описанием проблемы или пользовательским классом исключения.
Вот несколько примеров:
// выбрасываем литеральное целочисленное значение
throw -1;
// выбрасываем значение перечисления
throw ENUM_INVALID_INDEX;
// выбрасываем литеральную строку в стиле C (const char*)
throw “Can not take square root of negative number”;
// выбрасываем переменную double, определенную ранее
throw dX;
// выбрасываем объект класса MyException
throw MyException(“Fatal Error”);
Каждая из этих инструкций действует как сигнал о том, что возникла какая-то проблема, которую необходимо обработать.
Поиск исключений
Создание исключений – это только одна часть процесса обработки исключений. Вернемся к нашей аналогии с американским футболом: что происходит после того, как судья выбросил штрафной флажок? Игроки замечают штраф и останавливают игру. Нарушается нормальный ход футбольного матча.
В C++ мы используем ключевое слово try для определения блока инструкций (называемого блоком try). Блок try действует как наблюдатель, ищущий любые исключения, которые вызываются любой из инструкций в блоке try.
Вот пример блока try:
Обратите внимание, что блок try не определяет, КАК мы будем обрабатывать исключение. Он просто сообщает программе: «Эй, если какая-либо из инструкций внутри этого блока try вызовет исключение, захвати его!».
Обработка исключений
Наконец, конец нашей аналогии с американским футболом: после объявления штрафа и остановки игры судья определяет наказание и исполняет его. Другими словами, наказание должно быть выполнено до возобновления нормальной игры.
Фактически обработка исключений – это работа блока (ов) catch. Ключевое слово catch используется для определения блока кода (называемого блоком catch), который обрабатывает исключения для одного типа данных.
Вот пример блока catch, который перехватывает исключения со значениями int:
Блоки try и блоки catch работают вместе – блок try обнаруживает любые исключения, которые вызываются инструкциями в блоке try, и направляет их для обработки в соответствующий блок catch. Блок try должен иметь сразу после себя, по крайней мере, один блок catch, но он также может иметь несколько блоков catch, идущих последовательно.
Как только исключение было перехвачено блоком try и направлено в блок catch для обработки, исключение считается обработанным, и выполнение возобновится в обычном режиме после блока catch.
Параметры catch работают так же, как параметры функции, причем параметр доступен в последующем блоке catch. Исключения базовых типов можно перехватывать по значению, но исключения небазовых типов следует перехватывать по константной ссылке, чтобы избежать ненужного копирования.
Как и в случае с функциями, если параметр не будет использоваться в блоке catch, имя переменной можно не указывать:
Это может помочь предотвратить предупреждения компилятора о неиспользуемых переменных.
Throw, try и catch – собираем всё вместе
Вот полная программа, в которой используются инструкции throw, блок try и несколько блоков catch:
Выполнение показанного выше блока try/catch приведет к следующему результату:
We caught an int exception with value -1
Continuing on our merry way
Инструкция throw использовалась для вызова исключения со значением -1, которое имеет тип int. Затем эта инструкция throw была перехвачена включающим ее блоком try и направлена в соответствующий блок catch, который обрабатывает исключения типа int. Этот блок catch напечатал соответствующее сообщение об ошибке.
Как только исключение было обработано, программа продолжила работу в обычном режиме после блоков catch, напечатав “Continuing on our merry way”.
Обзор обработки исключений
Обработка исключений на самом деле довольно проста, и следующие два абзаца охватывают большую часть того, что вам нужно помнить о ней.
Когда выбрасывается исключение (с помощью throw), выполнение программы сразу же переходит к ближайшему, включающему эту инструкцию throw, блоку try (при необходимости с распространением вверх по стеку, чтобы найти охватывающий блок try – мы обсудим это более подробно в следующем уроке). Если какой-либо из обработчиков catch, прикрепленных к этому блоку try, обрабатывает этот тип исключения, то этот обработчик выполняется, и исключение считается обработанным.
Если подходящих обработчиков catch нет, выполнение программы переходит к следующему охватывающему блоку try. Если до конца программы не удается найти подходящего обработчика catch, то программа завершится с ошибкой исключения.
Обратите внимание, что при сопоставлении исключений с блоками catch компилятор не будет выполнять неявные преобразования или расширяющие преобразования (продвижения)! Например, исключение char не будет соответствовать блоку catch int. Исключение типа int не соответствует блоку catch float. Однако приведение производного класса к одному из его родительских классов будет выполнено.
Вот и всё, что нужно помнить. Остальная часть этой главы будет посвящена демонстрации работы этих принципов.
Исключения обрабатываются немедленно
Вот небольшая программа, демонстрирующая, как исключения обрабатываются немедленно:
Эта программа проста настолько, насколько это возможно. Вот что происходит: инструкция throw, первая выполняемая инструкция, вызывает исключение типа double. Выполнение немедленно переходит к ближайшему охватывающему ее блоку try, который является единственным блоком try в этой программе. Затем на совпадение проверяются обработчики catch. Наше исключение относится к типу double, поэтому мы ищем обработчик catch типа double. У нас есть один такой, поэтому он и выполняется.
Следовательно, результат этой программы будет следующим:
Обратите внимание, что “This never prints” никогда не печатается, потому что исключение привело к немедленному переходу порядка выполнения к обработчику исключения для значений double.
Более реалистичный пример
Давайте посмотрим на пример, который не совсем академичен:
В этом коде пользователя просят ввести число. Если он вводит положительное число, оператор if не выполняется, исключение не генерируется и печатается квадратный корень из этого числа. Поскольку в этом случае исключение не возникает, код внутри блока catch никогда не выполняется. Результат будет примерно таким:
Enter a number: 9
The sqrt of 9 is 3
Если пользователь вводит отрицательное число, мы генерируем исключение типа const char*. Поскольку мы находимся в блоке try и соответствующий обработчик исключений найден, управление немедленно передается обработчику исключений const char*. Результат:
Enter a number: -4
Error: Can not take sqrt of negative number
К настоящему моменту вы должны понять основную идею исключений. В следующем уроке мы приведем еще немало примеров, чтобы показать, насколько гибкими они являются.
Что обычно делают блоки catch
Если исключение направлено в блок catch, оно считается «обработанным», даже если этот блок catch пуст. Однако обычно вы захотите, чтобы блоки catch делали что-то полезное. Блоки catch выполняют три общих действия, когда перехватывают исключение:
- во-первых, блоки catch могут выводить сообщение об ошибке (либо в консоль, либо в лог-файл);
- во-вторых, блоки catch могут возвращать вызывающей функции значение или код ошибки.
- в-третьих, блок catch может выбросить другое исключение. Поскольку блок catch находится за пределами блока try, вновь созданное исключение в этом случае не обрабатывается предыдущим блоком try – оно обрабатывается следующим охватывающим блоком try.
Инварианты
Также блоки
try catch
позволяют производить обработку нескольких различных исключений, что вносит инвариантность в работу механизма исключений C++.
Например, класс вектор при создании может получить неправильный размер вектора или не найти свободную память для элементов, которые он будет содержать.
Данный конструктор может выбросить исключение в двух случаях:
- Если в качестве аргумента
size
будет передано отрицательное значение
- Если оператор
new
не сможет выделить память
length_error
– это стандартный оператор исключений, поскольку библиотека std часто использует данные исключения при своей работе.
Обработка исключений будет выглядеть следующим образом:
Также можно выделить свои собственные исключения.
Виды исключений
Все исключения стандартной библиотеки наследуются от
std::exception.
На данный момент существуют следующие виды исключений:
Определяет тип объекта, который будет брошен как исключение. Он сообщает об ошибках, которые являются следствием неправильной логики в рамках программы, такие как нарушение логической предпосылки или класс инвариантов, которые возможно предотвратить.
Этот класс используется как основа для ошибок, которые могут быть определены только во время выполнения программы.
Invalid_argument
Наследован от std::logic_error. Определяет исключение, которое должно быть брошено в случае неправильного аргумента.
Например, на MSDN приведён пример, когда в объект класса bitset из стандартной библиотеки
В данном примере передаётся неправильная строка, внутри которой имеется символ ‘b’, который будет ошибочным.
Наследован от std::logic_error. Определяет исключение, которое должно быть брошено в случае если математическая функция не определена для того аргумента, который ей передаётся, например:
Наследован от std::logic_error. Определяет исключение, которое должно быть броше в том случае, когда осуществляется попытка реализации превышения допустим пределов для объекта. Как это было показано для размера вектора в начале статьи.
Out_of_range
Наследован от std::logic_error. Определяет исключение, которое должно быть брошено в том случае, когда происходит выход за пределы допустимого диапазона значений объекта. Как это было показано для диапазона значений ветора в начале статьи.
Наследован от std::logic_error. Данное исключение может быть выброшено в том случае, если не удалось выполнить функцию, которая работает в асинхронном режиме и зависит от библиотеки потоков. Это исключение несет код ошибки совместимый с
std::error_code.
Исключение используется при ошибках при вычислении значений с плавающей запятой, когда компьютер не может обработать значение, поскольку оно является либо слишком большим, либо слишком маленьким. Если значение является значение интегрального типа, то должны использоваться исключения
underflow_error
или
overflow_error.
Исключение используется при ошибках при вычислении значений с плавающей запятой интегрального типа, когда число имеет слишком большое положительное значение, положительную бесконечность, при которой происходит потеря точности, т. результат настолько большой, что не может быть представлен числом в формате IEEE754.
Исключение используется при ошибках при вычислении значений с плавающей запятой интегрального типа, при которой происходит потеря точности, т. результат настолько мал, что не может быть представлен числом в формате IEEE754.
std::system_error
– это тип исключения, которое вызывается различными функциями стандартной библиотеки (как правило, функции, которые взаимодействуют с операционной системой, например, конструктор
std::thread
), при этом исключение имеет соответствующий
std::error_code.
Bad_typeid
Исключение этого типа возникает, когда оператор
typeid
применяется к нулевому указателю полиморфного типа.
Bad_cast
Данное исключение возникает в том случае, когда производится попытка каста объекта в тот тип объекта, который не входит с ним отношения наследования.
Bad_weak_ptr
Данное исключение генерируется в том случае, если был вызван метод
std::function::operator()
объекта
std::function
, который не получил объекта функции, то есть ему был передан в качестве инициализатора nullptr, например, а объект функции так и не был передан.
Bad_array_new_length
Исключение вызывается в следующих случаях:
- Массив имеет отрицательный размер
- Общий размер нового массива превысил максимальное значение, определяемое реализацией
- Количество элементов инициализации превышает предлагаемое количество инициализирующих элементов
Bad_exception
std::bad_exception
– это тип исключения в C++, которое выполняется в следующих ситуациях:
- Если нарушается динамическая спецификация исключений
- Если
std::exception_ptr
хранит копию пойманного исключения, и если конструктор копирования объекта исключения поймал current_exception, тогда генерируется исключение захваченных исключений.
Коды ошибок
Обработка ошибок неразрывно связана со способом передачи информации о возникших проблемах. Одним из вариантов сообщения об ошибке является возврат значения. Но даже в этом случае существует несколько подходов. Предположим, что мы пишем функцию для регистрации нового пользователя в системе. На вход мы передаем ей структуру с данными пользователя, а на выходе ожидаем получить идентификатор, который был ему присвоен.
Двойная ответственность
Вот как может выглядеть подобная функция:
Указатель для ошибки
Итак, логику мы подправили, но как теперь сообщить о произошедшей ошибке? Один из часто применяемых подходов заключается в том, чтобы вернуть информацию об ошибке через дополнительный параметр. Например:
Вот это поворот
Рассмотренный подход вполне справляется со своими задачами, но обычно для подобных функций в стиле C используют другую сигнатуру. Она выглядит следующим образом:
Кто там?
Со способом возврата ошибок с помощью переменных мы более или менее разобрались. Но я думаю, что вы уже могли заметить, что тип
bool в для подобных целей подходит не лучшим образом. Сейчас мы можем узнать лишь то, что функция отработала нормально или в ней произошла ошибка, но остается лишь догадываться, что именно пошло не так. Попробуем исправить это, добавив свой более информативный код ошибки. Обычно неплохим вариантом для решения подобной задачи являются перечисления:
Чуть выше
На самом деле, более простой код уже написать и не получится, конечно, если не воспользоваться каким-то принципиально другим механизмом возврата кодов ошибок. Более подробно о нем вы можете узнать из заметки, посвященной исключениям.
Заключение
На этом наше знакомство с исключениями законченно. Они и правда удобны, хотя и имеют свои недостатки. Однако в этой заметке мы в большей степени говорили об их преимуществах и вариантах использования, поэтому вполне вероятно, что выйдет еще одна часть по теме обработки ошибок, где мы обратим внимание на недостатки исключений, проведем более глубокое сравнение исключений и обычных кодов ошибок, а также разберемся с тем, в каких случаях исключения лучше не использовать.
(Пока оценок нет)
Наше знакомство с базовыми принципами обработки ошибок закончено. Мы успели рассмотреть основные способы возврата кодов ошибок; коротко вспомнили принцип создания C-оберток над ООП кодом C++ для создания переносимых библиотек; а также разобрались с возможными вариантами реализации функции-обработчика, которая должна проверять коды ошибок. Встретимся во второй части.
(2 оценок, среднее: 4,50 из 5)