11. Обработка исключений
Обработка исключений – это механизм, позволяющий двум независимо разработанным программным компонентам взаимодействовать в аномальной ситуации, называемой исключением. В этой главе мы расскажем, как генерировать, или возбуждать, исключение в том месте программы, где имеет место аномалия. Затем мы покажем, как связать catch-обработчик исключений с множеством инструкций программы, используя try-блок. Потом речь пойдет о спецификации исключений – механизме, с помощью которого можно связать список исключений с объявлением функции, и функция не сможет возбудить никаких других исключений. Закончится эта глава обсуждением решений, принимаемых при проектировании программы, в которой используются исключения.
11.1. Возбуждение исключения
– это аномальное поведение во время выполнения, которое программа может обнаружить, например: деление на 0, выход за границы массива или истощение свободной памяти. Такие исключения нарушают нормальный ход работы программы, и на них нужно немедленно отреагировать. В C++ имеются встроенные средства для их возбуждения и обработки. С помощью этих средств активизируется механизм, позволяющий двум несвязанным (или независимо разработанным) фрагментам программы обмениваться информацией об исключении.
Когда встречается аномальная ситуация, та часть программы, которая ее обнаружила, может сгенерировать, или возбудить, исключение. Чтобы понять, как это происходит, реализуем по-новому класс iStack, представленный в разделе 4.15, используя исключения для извещения об ошибках при работе со стеком. Определение класса
#include class iStack < public: iStack( int capacity ) : _stack( capacity ), _top( 0 ) < >bool pop( int &top_value ); bool push( int value ); bool full(); bool empty(); void display(); int size(); private: int _top; vector< int > _stack;
iStack выглядит следующим образом:
Стек реализован на основе вектора из элементов типа int. При создании объекта класса iStack его конструктор создает вектор из int, размер которого (максимальное число элементов, хранящихся в стеке) задается с помощью начального значения. Например, следующая инструкция создает объект myStack, который способен содержать не более 20 элементов типа int:
- запрашивается операция pop(), но стек пуст;
- запрашивается операция push(), но стек полон.
Вызвавшую функцию нужно уведомить об этих ошибках посредством исключений. С чего же начать?
Во-первых, мы должны определить, какие именно исключения могут быть возбуждены. В C++ они чаще всего реализуются с помощью классов. Хотя в полном объеме классы будут представлены в главе 13, мы все же определим здесь два из них, чтобы использовать их как исключения для класса iStack. Эти определения мы поместим в заголовочный файл stackExcp.h:
// stackExcp.h class popOnEmpty < /* . */ >; class pushOnFull < /* . */ >;
В главе 19 исключения в виде классов обсуждаются более подробно, там же рассматривается иерархия таких классов, предоставляемая стандартной библиотекой C++.
Затем надо изменить определения функций-членов pop() и push() так, чтобы они возбуждали эти исключения. Для этого предназначена инструкция throw, которая во многих отношениях напоминает return. Она состоит из ключевого слова throw, за которым следует выражение того же типа, что и тип возбуждаемого исключения. Как выглядит инструкция throw для функции pop()? Попробуем такой вариант:
// увы, это не совсем правильно throw popOnEmpty;
К сожалению, так нельзя. Исключение – это объект, и функция pop() должна генерировать объект класса соответствующего типа. Выражение в инструкции throw не может быть просто типом. Для создания нужного объекта необходимо вызвать конструктор класса. Инструкция throw для функции pop() будет выглядеть так:
// инструкция является вызовом конструктора throw popOnEmpty();
Эта инструкция создает объект исключения типа popOnEmpty. Напомним, что функции-члены pop() и push() были определены как возвращающие значение типа bool: true означало, что операция завершилась успешно, а false – что произошла ошибка. Поскольку теперь для извещения о неудаче pop() и push() используют исключения, возвращать значение необязательно. Поэтому мы будем считать, что эти функции-члены имеют тип void:
Теперь функции, пользующиеся нашим классом iStack, будут предполагать, что все хорошо, если только не возбуждено исключение; им больше не надо проверять возвращенное значение, чтобы узнать, как завершилась операция. В двух следующих разделах мы покажем, как определить функцию для обработки исключений, а сейчас представим новые реализации функций-членов pop() и push() класса iStack:
Хотя исключения чаще всего представляют собой объекты типа класса, инструкция throw может генерировать объекты любого типа. Например, функция mathFunc() в следующем примере возбуждает исключение в виде объекта-перечисления . Это корректный код C++:
Упражнение 11.1 Какие из приведенных инструкций throw ошибочны? Почему? Для правильных инструкций укажите тип возбужденного исключения:
(a) class exceptionType < >; throw exceptionType(); (b) int excpObj; throw excpObj; (c) enum mathErr < overflow, underflow, zeroDivide >; throw mathErr zeroDivide(); (d) int *pi = excpObj; throw pi;
Упражнение 11.2 У класса IntArray, определенного в разделе 2.3, имеется функция-оператор operator[](), в которой используется assert() для извещения о том, что индекс вышел за пределы массива. Измените определение этого оператора так, чтобы в подобной ситуации он генерировал исключение. Определите класс, который будет употребляться как тип возбужденного исключения.
11.2. Try-блок
В нашей программе тестируется определенный в предыдущем разделе класс iStack и его функции-члены pop() и push(). Выполняется 50 итераций цикла for. На каждой итерации в стек помещается значение, кратное 3: 3, 6, 9 и т.д. Если значение кратно 4 (4, 8, 12. ), то выводится текущее содержимое стека, а если кратно 10 (10, 20, 30. ), то с вершины снимается один элемент, после чего содержимое стека выводится снова. Как нужно изменить функцию main(), чтобы она обрабатывала исключения, возбуждаемые функциями-членами класса iStack?
Инструкции, которые могут возбуждать исключения, должны быть заключены в try-блок. Такой блок начинается с ключевого слова try, за которым идет последовательность инструкций, заключенная в фигурные скобки, а после этого – список обработчиков, называемых catch-предложениями. Try-блок группирует инструкции программы и ассоциирует с ними обработчики исключений. Куда нужно поместить try-блоки в функции main(), чтобы были обработаны исключения popOnEmpty и pushOnFull?
В таком виде программа выполняется корректно. Однако обработка исключений в ней перемежается с кодом, использующимся при нормальных обстоятельствах, а такая организация несовершенна. В конце концов, исключения – это аномальные ситуации, возникающие только в особых случаях. Желательно отделить код для обработки аномалий от кода, реализующего операции со стеком. Мы полагаем, что показанная ниже схема облегчает чтение и сопровождение программы:
- если исключение не возбуждено, то выполняется код внутри try-блока, а ассоциированные с ним обработчики игнорируются. Функция main() возвращает 0;
- если функция-член push(), вызванная из первой инструкции if внутри цикла for, возбуждает исключение, то вторая и третья инструкции if игнорируются, управление покидает цикл for и try-блок, и выполняется обработчик исключений типа pushOnFull;
- если функция-член pop(), вызванная из третьей инструкции if внутри цикла for, возбуждает исключение, то вызов display() игнорируется, управление покидает цикл for и try-блок, и выполняется обработчик исключений типа popOnEmpty.
Когда возбуждается исключение, пропускаются все инструкции, следующие за той, где оно было возбуждено. Исполнение программы возобновляется в catch-обработчике этого исключения. Если такого обработчика не существует, то управление передается в функцию terminate(), определенную в стандартной библиотеке C++. Try-блок может содержать любую инструкцию языка C++: как выражения, так и объявления. Он вводит локальную область видимости, так что объявленные внутри него переменные недоступны вне этого блока, в том числе и в catch-обработчиках. Например, функцию main() можно переписать так, что объявление переменной stack окажется в try-блоке. В таком случае обращаться к этой переменной в catch-обработчиках нельзя:
Можно объявить функцию так, что все ее тело будет заключено в try-блок. При этом не обязательно помещать try-блок внутрь определения функции, удобнее заключить ее тело в функциональный try-блок. Такая организация поддерживает наиболее чистое разделение кода для нормальной обработки и кода для обработки исключений. Например:
Обратите внимание, что ключевое слово try находится перед фигурной скобкой, открывающей тело функции, а catch-обработчики перечислены после закрывающей его скобки. Как видим, код, осуществляющий нормальную обработку, находится внутри тела функции и четко отделен от кода для обработки исключений. Однако к переменным, объявленным в main(), нельзя обратиться из обработчиков исключений.
Функциональный try-блок ассоциирует группу catch-обработчиков с телом функции. Если инструкция возбуждает исключение, то поиск обработчика, способного перехватить это исключение, ведется среди тех, что идут за телом функции. Функциональные try-блоки особенно полезны в сочетании с конструкторами классов. (Мы еще вернемся к этой теме в главе 19.)
- Прочитать первый файл и поместить в объект IntArray первое, третье, пятое, . n-ое значение (где n нечетно). Затем вывести содержимое объекта IntArray.
- Прочитать второй файл и поместить в объект IntArray пятое, десятое, . n-ое значение (где n кратно 5). Вывести содержимое объекта.
- Прочитать третий файл и поместить в объект IntArray второе, четвертое, . n-ое значение (где n четно). Вывести содержимое объекта.
Воспользуйтесь оператором operator[]() класса IntArray, определенным в упражнении 11.2, для сохранения и получения значений из объекта IntArray. Так как operator[]() может возбуждать исключения, обработайте их, поместив необходимое количество try-блоков и catch-обработчиков. Объясните, почему вы разместили try-блоки именно так, а не иначе.
11.3. Перехват исключений
В языке C++ исключения обрабатываются в предложениях catch. Когда какая-то инструкция внутри try-блока возбуждает исключение, то просматривается список последующих предложений catch в поисках такого, который может его обработать.
Catch-обработчик состоит из трех частей: ключевого слова catch, объявления одного типа или одного объекта, заключенного в круглые скобки (оно называется объявлением исключения), и составной инструкции. Если для обработки исключения выбрано некоторое catch-предложение, то выполняется эта составная инструкция. Рассмотрим catch-обработчики исключений pushOnFull и popOnEmpty в функции main() более подробно:
В обоих catch-обработчиках есть объявление типа класса; в первом это pushOnFull, а во втором – popOnEmpty. Для обработки исключения выбирается тот обработчик, для которого типы в объявлении исключения и в возбужденном исключении совпадают. (В главе 19 мы увидим, что типы не обязаны совпадать точно: обработчик для базового класса подходит и для исключений с производными классами.) Например, когда функция-член pop() класса iStack возбуждает исключение popOnEmpty, то управление попадает во второй обработчик. После вывода сообщения об ошибке в cerr, функция main() возвращает код errorCode89.
А если catch-обработчики не содержат инструкции return, с какого места будет продолжено выполнение программы? После завершения обработчика выполнение возобновляется с инструкции, идущей за последним catch-обработчиком в списке. В нашем примере оно продолжается с инструкции return в функции main(). После того как catch-обработчик popOnEmpty выведет сообщение об ошибке, main() вернет 0.
Говорят, что механизм обработки исключений в C++ невозвратный: после того как исключение обработано, управление не возобновляется с того места, где оно было возбуждено. В нашем примере управление не возвращается в функцию-член pop(), возбудившую исключение.
11.3.1. Объекты-исключения
Объявлением исключения в catch-обработчике могут быть объявления типа или объекта. В каких случаях это следует делать? Тогда, когда необходимо получить значение или как-то манипулировать объектом, созданным в выражении throw. Если классы исключений спроектированы так, что в объектах-исключениях при возбуждении сохраняется некоторая информация и если в объявлении исключения фигурирует такой объект, то инструкции внутри catch-обработчика могут обращаться к информации, сохраненной в объекте выражением throw.
Изменим реализацию класса исключения pushOnFull, сохранив в объекте-исключении то значение, которое не удалось поместить в стек. Catch-обработчик, сообщая об ошибке, теперь будет выводить его в cerr. Для этого мы сначала модифицируем определение типа класса pushOnFull следующим образом:
// новый класс исключения: // он сохраняет значение, которое не удалось поместить в стек class pushOnFull < public: pushOnFull( int i ) : _value( i ) < >int value < return _value; >private: int _value; >;
Новый закрытый член _value содержит число, которое не удалось поместить в стек. Конструктор принимает значение типа int и сохраняет его в члене _data. Вот как вызывается этот конструктор для сохранения значения из выражения throw:
У класса pushOnFull появилась также новая функция-член value(), которую можно использовать в catch-обработчике для вывода хранящегося в объекте-исключении значения:
catch ( pushOnFull eObj )
Обратите внимание, что в объявлении исключения в catch-обработчике фигурирует объект eObj, с помощью которого вызывается функция-член value() класса pushOnFull.
Объект-исключение всегда создается в точке возбуждения, даже если выражение throw – это не вызов конструктора и, на первый взгляд, не должно создавать объекта. Например:
enum EHstate < noErr, zeroOp, negativeOp, severeError >; enum EHstate state = noErr; int mathFunc( int i ) < if ( i == 0 ) < state = zeroOp; throw state; // создан объект-исключение >// иначе продолжается обычная обработка >
В этом примере объект state не используется в качестве объекта-исключения. Вместо этого выражением throw создается объект-исключение типа EHstate, который инициализируется значением глобального объекта state. Как программа может различить их? Для ответа на этот вопрос мы должны присмотреться к объявлению исключения в catch-обработчике более внимательно.
Это объявление ведет себя почти так же, как объявление формального параметра. Если при входе в catch-обработчик исключения выясняется, что в нем объявлен объект, то он инициализируется копией объекта-исключения. Например, следующая функция calculate() вызывает определенную выше mathFunc(). При входе в catch-обработчик внутри calculate() объект eObj инициализируется копией объекта-исключения, созданного выражением throw.
Объявление исключения в этом примере напоминает передачу параметра по значению. Объект eObj инициализируется значением объекта-исключения точно так же, как переданный по значению формальный параметр функции – значением соответствующего фактического аргумента. (Передача параметров по значению рассматривалась в разделе 7.3)
Как и в случае параметров функции, в объявлении исключения может фигурировать ссылка. Тогда catch-обработчик будет напрямую ссылаться на объект-исключение, сгенерированный выражением throw, а не создавать его локальную копию:
Для предотвращения ненужного копирования больших объектов применять ссылки следует не только в объявлениях параметров типа класса, но и в объявлениях исключений того же типа.
В последнем случае catch-обработчик сможет модифицировать объект-исключение. Однако переменные, определенные в выражении throw, остаются без изменения. Например, модификация eObj внутри catch-обработчика не затрагивает глобальную переменную state, установленную в выражении throw:
Catch-обработчик переустанавливает eObj в noErr после исправления ошибки, вызвавшей исключение. Поскольку eObj – это ссылка, можно ожидать, что присваивание модифицирует глобальную переменную state. Однако изменяется лишь объект-исключение, созданный в выражении throw, поэтому модификация eObj не затрагивает state.
11.3.2. Раскрутка стека
Поиск catch-обработчикадля возбужденного исключения происходит следующим образом. Когда выражение throw находится в try-блоке, все ассоциированные с ним предложения catch исследуются с точки зрения того, могут ли они обработать исключение. Если подходящее предложение catch найдено, то исключение обрабатывается. В противном случае поиск продолжается в вызывающей функции. Предположим, что вызов функции, выполнение которой прекратилось в результате исключения, погружен в try-блок; в такой ситуации исследуются все предложения catch, ассоциированные с этим блоком. Если один из них может обработать исключение, то процесс заканчивается. В противном случае переходим к следующей по порядку вызывающей функции. Этот поиск последовательно проводится во всей цепочке вложенных вызовов. Как только будет найдено подходящее предложение, управление передается в соответствующий обработчик.
В нашем примере первая функция, для которой нужен catch-обработчик, – это функция-член pop() класса iStack. Поскольку выражение throw внутри pop() не находится в try-блоке, то программа покидает pop(), не обработав исключение. Следующей рассматривается функция, вызвавшая pop(), то есть main(). Вызов pop() внутри main() находится в try-блоке, и далее исследуется, может ли хотя бы одно ассоциированное с ним предложение catch обработать исключение. Поскольку обработчик исключения popOnEmpty имеется, то управление попадает в него.
Процесс, в результате которого программа последовательно покидает составные инструкции и определения функций в поисках предложения catch, способного обработать возникшее исключение, называется раскруткой стека. По мере раскрутки прекращают существование локальные объекты, объявленные в составных инструкциях и определениях функций, из которых произошел выход. C++ гарантирует, что во время описанного процесса вызываются деструкторы локальных объектов классов, хотя они исчезают из-за возбужденного исключения. (Подробнее мы поговорим об этом в главе 19.)
Если в программе нет предложения catch, способного обработать исключение, оно остается необработанным. Но исключение – это настолько серьезная ошибка, что программа не может продолжать выполнение. Поэтому, если обработчик не найден, вызывается функция terminate() из стандартной библиотеки C++. По умолчанию terminate() активизирует функцию abort(), которая аномально завершает программу. (В большинстве ситуаций вызов abort() оказывается вполне приемлемым решением. Однако иногда необходимо переопределить действия, выполняемые функцией terminate(). Как это сделать, рассказывается в книге [STROUSTRUP97].)
Вы уже, наверное, заметили, что обработка исключений и вызов функции во многом похожи. Выражение throw ведет себя аналогично вызову, а предложение catch чем-то напоминает определение функции. Основная разница между этими двумя механизмами заключается в том, что информация, необходимая для вызова функции, доступна во время компиляции, а для обработки исключений – нет. Обработка исключений в C++ требует языковой поддержки во время выполнения. Например, для обычного вызова функции компилятору в точке активизации уже известно, какая из перегруженных функций будет вызвана. При обработке же исключения компилятор не знает, в какой функции находится catch-обработчик и откуда возобновится выполнение программы. Функция terminate() предоставляет механизм времени выполнения, который извещает пользователя о том, что подходящего обработчика не нашлось.
11.3.3. Повторное возбуждение исключения
Может оказаться так, что в одном предложении catch не удалось полностью обработать исключение. Выполнив некоторые корректирующие действия, catch-обработчик может решить, что дальнейшую обработку следует поручить функции, расположенной «выше» в цепочке вызовов. Передать исключение другому catch-обработчику можно с помощью повторного возбуждения исключения. Для этой цели в языке предусмотрена конструкция
которая вновь генерирует объект-исключение. Повторное возбуждение возможно только внутри составной инструкции, являющейся частью catch-обработчика:
При повторном возбуждении новый объект-исключение не создается. Это имеет значение, если catch-обработчик модифицирует объект, прежде чем возбудить исключение повторно. В следующем фрагменте исходный объект-исключение не изменяется. Почему?
Так как eObj не является ссылкой, то catch-обработчик получает копию объекта-исключения, так что любые модификации eObj относятся к локальной копии и не отражаются на исходном объекте-исключении, передаваемом при повторном возбуждении. Таким образом, переданный далее объект по-прежнему имеет тип zeroOp.
Чтобы модифицировать исходный объект-исключение, в объявлении исключения внутри catch-обработчика должна фигурировать ссылка:
Теперь eObj ссылается на объект-исключение, созданный выражением throw, так что все изменения относятся непосредственно к исходному объекту. Поэтому при повторном возбуждении исключения далее передается модифицированный объект.
Таким образом, другая причина для объявления ссылки в catch-обработчике заключается в том, что сделанные внутри обработчика модификации объекта-исключения в таком случае будут видны при повторном возбуждении исключения. (Третья причина будет рассмотрена в разделе 19.2, где мы расскажем, как catch-обработчик вызывает виртуальные функции класса.)
11.3.4. Перехват всех исключений
Иногда функции нужно выполнить определенное действие до того, как она завершит обработку исключения, даже несмотря на то, что обработать его она не может. К примеру, функция захватила некоторый ресурс, скажем открыла файл или выделила память из хипа, и этот ресурс необходимо освободить перед выходом:
Если исключение возбуждено, то управление не попадет на инструкцию, где ресурс освобождается. Чтобы освободить ресурс, не пытаясь перехватить все возможные исключения (тем более, что мы не всегда знаем, какие именно исключения могут возникнуть), воспользуемся специальной конструкцией, позволяющей перехватывать любые исключения. Это не что иное, как предложение catch, в котором объявление исключения имеет вид (. ) и куда управление попадает при любом исключении. Например:
// управление попадает сюда при любом возбужденном исключении catch (. ) < // здесь размещаем наш код >
Конструкция catch(. ) используется в сочетании с повторным возбуждением исключения. Захваченный ресурс освобождается внутри составной инструкции в catch-обработчике перед тем, как передать исключение по цепочке вложенных вызовов в результате повторного возбуждения:
Чтобы гарантировать освобождение ресурса в случае, когда выход из manip() происходит в результате исключения, мы освобождаем его внутри catch(. ) до того, как исключение будет передано дальше. Можно также управлять захватом и освобождением ресурса путем инкапсуляции в класс всей работы с ним. Тогда захват будет реализован в конструкторе, а освобождение – в автоматически вызываемом деструкторе. (С этим подходом мы познакомимся в главе 19.)
Предложение catch(. ) используется самостоятельно или в сочетании с другими catch-обработчиками. В последнем случае следует позаботиться о правильной организации обработчиков, ассоциированных с try-блоком.
Catch-обработчики исследуются по очереди, в том порядке, в котором они записаны. Как только найден подходящий, просмотр прекращается. Следовательно, если предложение catch(. ) употребляется вместе с другими catch-обработчиками, то оно должно быть последним в списке, иначе компилятор выдаст сообщение об ошибке:
try < stack.display(); for ( int ix = 1; ix < 51; ++x ) < // то же, что и выше >> catch ( pushOnFull ) < >catch ( popOnEmpty ) < >catch ( . ) < >// должно быть последним в списке catch-обработчиков
Упражнение 11.4 Объясните, почему модель обработки исключений в C++ называется невозвратной. Упражнение 11.5 Даны следующие объявления исключений. Напишите выражения throw, создающие объект-исключение, который может быть перехвачен указанными обработчиками:
(a) class exceptionType < >; catch( exceptionType *pet ) < >(b) catch(. ) < >(c) enum mathErr < overflow, underflow, zeroDivide >; catch( mathErr &ref ) < >(d) typedef int EXCPTYPE; catch( EXCPTYPE ) < >Упражнение 11.6 Объясните, что происходит во время раскрутки стека. Упражнение 11.7 Назовите две причины, по которым объявление исключения в предложении catch следует делать ссылкой. Упражнение 11.8
На основе кода, написанного вами в упражнении 11.3, модифицируйте класс созданного исключения: неправильный индекс, использованный в операторе operator[](), должен сохраняться в объекте-исключении и затем выводиться catch-обработчиком. Измените программу так, чтобы operator[]() возбуждал при ее выполнении исключение.
11.4. Спецификации исключений
По объявлениям функций-членов pop() и push() класса iStack невозможно определить, что они возбуждают исключения. Можно, конечно, включить в объявление подходящий комментарий. Тогда описание интерфейса класса в заголовочном файле будет содержать документацию возбуждаемых исключений:
Но такое решение несовершенно. Неизвестно, будет ли обновлена документация при выпуске следующих версий iStack. Кроме того, комментарий не дает компилятору достоверной информации о том, что никаких других исключений функция не возбуждает. Спецификация исключений позволяет перечислить в объявлении функции все исключения, которые она может возбуждать. При этом гарантируется, что другие исключения функция возбуждать не будет.
Такая спецификация следует за списком формальных параметров функции. Она состоит из ключевого слова throw, за которым идет список типов исключений, заключенный в скобки. Например, объявления функций-членов класса iStack можно модифицировать, добавив спецификации исключений:
Гарантируется, что при обращении к pop() не будет возбуждено никаких исключений, кроме popOnEmpty, а при обращении к push()–только pushOnFull.
Объявление исключения – это часть интерфейса функции, оно должно быть задано при ее объявлении в заголовочном файле. Спецификация исключений – это своего рода «контракт» между функцией и остальной частью программы, гарантия того, что функция не будет возбуждать никаких исключений, кроме перечисленных.
Если в объявлении функции присутствует спецификация исключений, то при повторном объявлении этой же функции должны быть перечислены точно те же типы. Спецификации исключений в разных объявлениях одной и той же функции не суммируются:
// два объявления одной и той же функции extern int foo( int = 0 ) throw(string); // ошибка: опущена спецификация исключений extern int foo( int parm )
Что произойдет, если функция возбудит исключение, не перечисленное в ее спецификации? Исключения возбуждаются только при обнаружении определенных аномалий в поведении программы, и во время компиляции неизвестно, встретится ли то или иное исключение во время выполнения. Поэтому нарушения спецификации исключений функции могут быть обнаружены только во время выполнения. Если функция возбуждает исключение, не указанное в спецификации, то вызывается unexpected() из стандартной библиотеки C++, а та по умолчанию вызывает terminate(). (В некоторых случаях необходимо переопределить действия, выполняемые функцией unexpected(). Стандартная библиотека предоставляет механизм для этого. Подробнее см. [STRAUSTRUP97].)
Необходимо уточнить, что unexpected() не вызывается только потому, что функция возбудила исключение, не указанное в ее спецификации. Все нормально, если она обработает это исключение самостоятельно, внутри функции. Например:
void recoup( int op1, int op2 ) throw(ExceptionType) < try < // . throw string("we're in control"); >// обрабатывается возбужденное исключение catch ( string ) < // сделать все необходимое >> // все хорошо, unexpected() не вызывается
Функция recoup() возбуждает исключение типа string, несмотря на его отсутствие в спецификации. Поскольку это исключение обработано в теле функции, unexpected() не вызывается.
Нарушения спецификации исключений функции обнаруживаются только во время выполнения. Компилятор не сообщает об ошибке, если в выражении throw возбуждается исключение неуказанного типа. Если такое выражение никогда не выполнится или не возбудит исключения, нарушающего спецификацию, то программа будет работать, как и ожидалось, и нарушение никак не проявится:
extern void doit( int, int ) throw(string, exceptionType); void action ( int op1, int op2 ) throw(string) < doit( op1, op2 ); // ошибки компиляции не будет // . >
doit() может возбудить исключение типа exceptionType, которое не разрешено спецификацией action(). Однако функция компилируется успешно. Компилятор при этом генерирует код, гарантирующий, что при возбуждении исключения, нарушающего спецификацию, будет вызвана библиотечная функция unexpected(). Пустая спецификация показывает, что функция не возбуждает никаких исключений:
extern void no_problem () throw();
Если же в объявлении функции спецификация исключений отсутствует, то может быть возбуждено исключение любого типа. Между типом возбужденного исключения и типом исключения, указанного в спецификации, не разрешается проводить никаких преобразований:
Выражение throw в функции convert() возбуждает исключение типа строки символов в стиле языка C. Созданный объект-исключение имеет тип const char*. Обычно выражение типа const char* можно привести к типу string. Однако спецификация не допускает преобразования типов, поэтому если convert() возбуждает такое исключение, то вызывается unexpected(). Для исправления ошибки выражение throw можно модифицировать так, чтобы оно явно преобразовывало значение выражения в тип string:
throw string( «help!» );
11.4.1. Спецификации исключений и указатели на функции
Спецификацию исключений можно задавать и при объявлении указателя на функцию. Например:
void (*pf)( int ) throw(string);
В этом объявлении говорится, что pf указывает на функцию, которая способна возбуждать только исключения типа string. Как и для объявлений функций, спецификации исключений в разных объявлениях одного и того же указателя не суммируются, они должны быть одинаковыми:
extern void (*pf) ( int ) throw(string); // ошибка: отсутствует спецификация исключения void (*pf)( int );
При работе с указателем на функцию со спецификацией исключений есть ограничения на тип указателя, используемого в качестве инициализатора или стоящего в правой части присваивания. Спецификации исключений обоих указателей не обязаны быть идентичными. Однако на указатель-инициализатор она должна накладывать столь же или более строгие ограничения, чем на инициализируемый указатель (или тот, которому присваивается значение). Например:
void recoup( int, int ) throw(exceptionType); void no_problem() throw(); void doit( int, int ) throw(string, exceptionType); // правильно: ограничения, накладываемые на спецификации // исключений recoup() и pf1, одинаковы void (*pf1)( int, int ) throw(exceptionType) = &recoup; // правильно: ограничения, накладываемые на спецификацию исключений no_problem(), более строгие, // чем для pf2 void (*pf2)( ) throw(string) = &no_problem; // ошибка: ограничения, накладываемые на спецификацию // исключений doit(), менее строгие, чем для pf3 // void (*pf3)( int, int ) throw(string) = &doit;
Третья инициализация не имеет смысла. Объявление указателя гарантирует, что pf3 адресует функцию, которая может возбуждать только исключения типа string. Но doit() возбуждает также исключения типа exceptionType. Поскольку она не подходит под ограничения, накладываемые спецификацией исключений pf3, то не может служить корректным инициализатором для pf3, так что компилятор выдает ошибку. Упражнение 11.9
В коде, разработанном для упражнения 11.8, измените объявление оператора operator[]() в классе IntArray, добавив спецификацию возбуждаемых им исключений. Модифицируйте программу так, чтобы operator[]() возбуждал исключение, не указанное в спецификации. Что при этом происходит? Упражнение 11.10
Какие исключения может возбуждать функция, если ее спецификация исключений имеет вид throw()? А если у нее нет такой спецификации? Упражнение 11.11 Какое из следующих присваиваний ошибочно? Почему?
void example() throw(string); (a) void (*pf1)() = example; (b) void (*pf2) throw() = example;
11.5. Исключения и вопросы проектирования
С обработкой исключений в программах C++ связано несколько вопросов. Хотя поддержка такой обработки встроена в язык, не стоит использовать ее везде. Обычно она применяется для обмена информацией об ошибках между независимо разработанными частями программы. Например, автор некоторой библиотеки может с помощью исключений сообщать пользователям об ошибках. Если библиотечная функция обнаруживает аномальную ситуацию, которую не способна обработать самостоятельно, она может возбудить исключение для уведомления вызывающей программы.
В нашем примере в библиотеке определен класс iStack и его функции-члены. Разумно предположить, что программист, кодировавший main(), где используется эта библиотека, не разрабатывал ее. Функции-члены класса iStack могут обнаружить, что операция pop() вызвана, когда стек пуст, или что операция push() вызвана, когда стек полон; однако разработчик библиотеки ничего не знал о программе, пользующейся его функциями, так что не мог разрешить проблему локально. Не сумев обработать ошибку внутри функций-членов, мы решили возбуждать исключения, чтобы известить вызывающую программу.
Хотя C++ поддерживает исключения, следует применять и другие методы обработки ошибок (например, возврат кода ошибки) – там, где это более уместно. Однозначного ответа на вопрос: «Когда ошибку следует трактовать как исключение?» не существует. Ответственность за решение о том, что считать исключительной ситуацией, возлагается на разработчика. Исключения – это часть интерфейса библиотеки, и решение о том, какие исключения она возбуждает, – важный аспект ее дизайна. Если библиотека предназначена для использования в программах, которые не должны аварийно завершаться ни при каких обстоятельствах, то она обязана разбираться с аномалиями сама либо извещать о них вызывающую программу, передавая ей управление. Решение о том, какие ошибки следует обрабатывать как исключения, – трудная часть работы по проектированию библиотеки.
В нашем примере с классом iStack вопрос, должна ли функция push() возбуждать исключение, если стек полон, является спорным. Альтернативная и, по мнению многих, лучшая реализация push() – локальное решение проблемы: увеличение размера стека при его заполнении. В конце концов, единственное ограничение – это объем доступной программе памяти. Наше решение о возбуждении исключения при попытке поместить значение в полный стек, по-видимому, непродуманно. Можно переделать функцию-член push(), чтобы она в такой ситуации наращивала стек:
Аналогично следует ли функции pop() возбуждать исключение при попытке извлечь значение из пустого стека? Интересно отметить, что класс stack из стандартной библиотеки C++ (он рассматривался в главе 6) не возбуждает исключения в такой ситуации. Вместо этого постулируется, что поведение программы при попытке выполнения подобной операции не определено. Разрешить программе продолжать работу при обнаружении некорректного состояния признали возможным. Мы уже упоминали, что в разных библиотеках определены разные исключения. Не существует пригодного для всех случаев ответа на вопрос, что такое исключение.
Не все программы должны беспокоиться по поводу исключений, возбуждаемых библиотечными функциями. Хотя есть системы, для которых простой недопустим и которые, следовательно, должны обрабатывать все исключительные ситуации, не к каждой программе предъявляются такие требования. Обработка исключений предназначена в первую очередь для реализации отказоустойчивых систем. В этом случае решение о том, должна ли программа обрабатывать все исключения, возбуждаемые библиотеками, или может закончить выполнение аварийно, – это трудная часть процесса проектирования.
Еще один аспект проектирования программ заключается в том, что обработка исключений обычно структурирована. Как правило, программа строится из компонентов, и каждый компонент решает сам, какие исключения обрабатывать локально, а какие передавать на верхние уровни. Что мы понимаем под компонентом? Например, система анализа текстовых запросов, рассмотренная в главе 6, может быть разбита на три компонента, или слоя. Первый слой – это стандартная библиотека C++, которая обеспечивает базовые операции над строками, отображениями и т.д. Второй слой – это сама система анализа текстовых запросов, где определены такие функции, как string_caps() и suffix_text(), манипулирующие текстами и использующие стандартную библиотеку как основу. Третий слой – это программа, которая применяет нашу систему. Каждый компонент строится независимо и должен принимать решения о том, какие исключительные ситуации обрабатывать локально, а какие передавать на более высокий уровень.
Не все функции должны уметь обрабатывать исключения. Обычно try-блоки и ассоциированные с ними catch-обработчики применяются в функциях, являющихся точками входа в компонент. Catch-обработчики проектируются так, чтобы перехватывать те исключения, которые не должны попасть на верхние уровни программы. Для этого также используются спецификации исключений (см. раздел 11.4).
Мы расскажем о других аспектах проектирования программ, использующих исключения, в главе 19, после знакомства с классами и иерархиями классов.
Лучшие практики обработки исключений в C#
В рамках скорого старта курса «C# Developer. Professional» подготовили для вас перевод материала.
Приглашаем также всех желающих на бесплатный демо-урок «DI-контейнеры для C#». На этом занятии мы:
1) Разберемся с тем, что такое принцип DI и зачем он нужен;
2) Научимся применять DI без использования контейнеров;
3) Рассмотрим два популярных DI-контейнеры для C#: Windsor и Autofac, разберем их плюсы и минусы;
4) Научимся регистрировать зависимости, управлять их жизненным циклом, применять инъекцию зависимостей.
Я плавно приближаюсь к своему двадцатилетнему юбилею в технической индустрии. На протяжении этих лет я своими глазами повидал почти все анти-паттерны обработки исключений (да что уж там, и я сам тоже совершал ошибки). В этой статье я собрал собственные лучшие практики работы с исключениями в C#.
Не генерируйте исключения повторно
Я натыкаюсь на это снова и снова. Люди оказываются сбиты с толку тем, что исходный стек трейс «волшебным образом» исчезает при обработке ошибок. Чаще всего это вызвано повторной генерацией исключений. Давайте посмотрим на пример, в котором у нас есть вложенные try/catch :
Как вы, наверное, уже догадались, внутренний try/catch перехватывает, регистрирует и проглатывает исключение. Чтобы пробросить SpecificException в глобальный блок catch для его обработки, вам нужно пробросить его в стек. Вы можете сделать следующее:
Основное отличие здесь состоит в том, что в первом примере повторно генерируется SpecificException , что приводит к сбросу стек трейса исходного исключения, в то время как второй пример сохраняют все детали исходного исключения. Почти всегда предпочтительнее использовать второй пример.
Декорируйте исключения
Я достаточно редко вижу реализацию этой рекомендации на практике. Все исключения расширяют Exception, в котором есть словарь Data. Словарь можно использовать для включения дополнительной информации об ошибке. Отображается ли эта информация в вашем логе, зависит от того, какой фреймворк логирования и хранилище вы используете. В elmah.io записи Data отображаются на вкладке Data.
Информацию в словарь Data вносится посредством добавьте пар ключ/значение:
В этом примере я добавляю ключ с именем user с потенциальным именем пользователя, хранящимся в потоке.
Вы также можете декорировать исключения, сгенерированные сторонним кодом. Добавьте try/catch :
Код перехватывает любые исключения, генерируемые методом SomeCall , и добавляет в них имя пользователя. Посредством добавления ключевого слова throw в блок catch исходное исключение пробрасывается дальше по стеку.
Перехватывайте в первую очередь наиболее специфические исключения
Вероятнее всего, у вас есть где-то код, похожий на этот:
Простой перехват Exception и логирование его в предпочитаемом фреймворке быстро реализуются и справляются со своей задачей. Большинство библиотек, доступных в .NET, могут генерировать ряд различных исключений, и у вас может даже уже быть похожий шаблон в вашей кодовой базе. Перехват нескольких исключений в диапазоне от наиболее до наименее специфической ошибки — отличный способ определить, как вы хотите обрабатывать каждый конкретный тип исключения.
В следующем примере я четко демонстрирую понимание, какие исключения следует ожидать и как поступать с каждым конкретным типом:
Перехватывая ArgumentException и DirectoryNotFoundException перед перехватом общего Exception , я могу показать пользователю специализированное сообщение. В этих сценариях я не регистрирую исключение, поскольку пользователь может быстро исправить ошибки. В случае Exception я генерирую support id , регистрирую ошибку (используя декораторы, как показано в предыдущем разделе) и показываю сообщение пользователю.
Обратите внимание, что, хотя приведенный выше код служит для объяснения порядка обработки исключений, реализация потока управления, используя исключения подобным образом — практика не очень хорошая. Это прекрасная подводка к следующему совету:
Старайтесь избегать исключений
Может показаться очевидным, что нужно избегать исключений. Но многих методов, генерирующих исключение, можно избежать с помощью защитного программирования.
Одно из самых распространенных исключений — NullReferenceException . В некоторых случаях вы можете разрешить null , но забыть проверить на null . Вот пример, который генерирует NullReferenceException :
Доступ к a выбрасывает исключение. Хорошо, но представьте, что a предоставляется в качестве параметра.
Если вы хотите разрешить city с нулевым значением, вы можете избежать исключения, используя null-condition оператор:
Добавляя ? при доступе к a C# автоматически обрабатывает сценарий, в котором адрес равен null . В этом случае переменной city будет присвоено значение null .
Другой распространенный пример исключений — это анализ чисел или логических значений. В следующем примере будет сгенерировано FormatException :
Строка invalid не может быть распаршена в виде целого числа. Чтобы не оборачивать это в try/catch , int предоставляет интересный метод, который вы, вероятно, уже использовали 1000 раз:
В случае, если invalid может быть распаршена как int , TryParse возвращает true и помещает распаршенное значение в переменную i . Еще одно исключение удалось избежать.
Создавайте пользовательские исключения
Забавно вспоминать, как я был Java-программистом (когда .NET находился в стадии бета-тестирования). Мы создавали собственные пользовательские исключения для всего чего угодно. Возможно, это происходило из-за более явной реализации исключений в Java, но я не вижу этого в .NET и C#. Создавая пользовательское исключение, у вас гораздо больше возможностей для перехвата определенных исключений, как уже было показано. Вы можете декорировать свое исключение пользовательскими переменными, не беспокоясь о том, поддерживает ли ваш логгер словарь Data:
Класс MyVerySpecializedException (возможно, это не то имя класса, которое вы должны использовать в качестве примера :D) реализует три конструктора, которые должен иметь каждый класс исключения. Кроме того, я добавил свойство Status в качестве примера дополнительных данных. Это позволит нам написать такой код:
Используя ключевое слово when , я могу перехватить MyVerySpecializedException , когда значение свойства Status равно 500. Все остальные сценарии попадут в общий catch MyVerySpecializedException .
Логируйте исключения
Это кажется таким очевидным. Но я видел слишком много ошибок в коде в следующих строках при использовании этого шаблона:
Логирование как неперехваченных, так и перехваченных исключений — это меньшее, что вы можете сделать для своих пользователей. Нет ничего хуже, чем когда пользователи обращаются в вашу службу поддержки, и вы даже не подозреваете, какие были ошибки и что произошло. В этом вам поможет ведение логов.
Name already in use
dotnetbook / book / ru / Execution / 2-ExceptionalFlow / 1-Exceptions-Intro.md
- Go to file T
- Go to line L
- Copy path
- Copy permalink
- Open with Desktop
- View raw
- Copy raw contents Copy raw contents
Copy raw contents
Copy raw contents
В нашем разговоре о потоке исполнения команд различными подсистемами пришло время поговорить про исключения или, скорее, исключительные ситуации. И прежде чем продолжить стоит совсем немного остановиться именно на самом определении. Что такое исключительная ситуация?
Исключительной называют такую ситуацию, которая делает исполнение дальнейшего кода абсолютно не корректным. Не таким как задумывалось, проектировалось. Переводит состояние приложения в целом или же его отдельной части (например, объекта) в состояние нарушенной целостности. Т.е. что-то экстраординарное, исключительное.
Почему же это так важно — определить терминологию? Работа с терминологией очень важна, т.к. она держит нас в рамках, а работу кода — предсказуемой для стороннего разработчика. Если не следовать терминологии можно уйти далеко от созданного проектировщиками концепта и получить множество неоднозначных ситуаций как для себя так и для других. А чтобы закрепить понимание вопроса давайте обратимся к примеру:
Этот пример кажется немного странным: и это не просто так. Для того чтобы показать исключительность проблем, возникающих в данном коде я сделал его несколько утрированным. Для начала посмотрим на метод Parse . Почему он должен выбрасывать исключение?
- Он принимает в качестве параметра строку, а в качестве результата — некоторое число, которое является значимым типом. По этому числу мы никак не можем определить, является ли оно результатом корректных вычислений или же нет: оно просто есть. Другими словами, в интерфейсе метода отсутствует возможность сообщить о проблеме;
- С другой стороны метод, принимая строку подразумевает что её для него корректно подготовили: там нет лишних символов и строка содержит некоторое число. Если это не так, то возникает проблема предусловий к методу: тот код, который вызвал наш метод отдал ему не корректные данные.
Получается, что для данного метода ситуация получения строки с не корректными данными является исключительной: метод не может вернуть корректного значения, но и вернуть абы что он не может. А потому единственный выход для него — бросить исключение.
Второй вариант метода обладает каналом сигнализации о наличии проблем с входными данными: возвращаемое значение тут boolean и является признаком успешности выполнения метода. Сигнализировать о каких-либо проблемах при помощи механизма исключений данный метод не имеет ни малейшего повода: все виды проблем легко уместятся в возвращаемое значение false .
Обработка исключительных ситуаций может показаться вопросом достаточно элементарным: ведь все что нам необходимо сделать — это установить try-catch блоки и ждать соответствующего события. Однако вопрос кажется элементарным только благодаря огромной работе, проделанной командами CLR и CoreCLR чтобы унифицировать все ошибки, которые лезут в CLR со всех щелей — из самых разных источников. Чтобы иметь представление, о чем мы будем далее вести беседу, давайте взглянем на диаграмму:

устарело: нет Linux
На этой схеме мы видим, что в .NET существует по сути два мира: все, что относится к CLR и все, что находится за ней: все возможные ошибки, возникающие в Windows / Linux и прочем unsafe мире:
- Structured Exception Handling (SEH) — структурированная обработка исключений — стандарт платформы Windows для обработки исключений. Во время вызовов unsafe методов и последующем выбросе исключений происходит конвертация исключений unsafe <-> CLR в обе стороны: из unsafe в CLR и обратно, т.к. CLR может вызвать unsafe метод, а тот в свою очередь — CLR метод.
- Vectored Exception Handling (VEH) — по своей сути является корнем SEH, позволяя вставлять свои обработчики в точку выброса исключения. Используется в частности для установки FirstChanceException .
- COM+ исключения — когда источником проблемы является некоторый COM компонент, то прослойка между COM и .NET методом должна сконвертировать COM ошибку в исключение .NET
- И, наконец, обёртки для HRESULT. Введены для конвертации модели WinAPI (код ошибки — в возвращаемом значении, а возвращаемые значения — через параметры метода) в модель исключений: для .NET в отличии от операционных систем стандартом является именно исключительная ситуация
С другой стороны, поверх CLI располагаются языки программирования, каждый из которых частично или же полностью — предлагает функционал по обработке исключений конечному пользователю языка. Так, например, языки VB.NET и F# до недавнего времени обладали более богатым функционалом по части обработки исключительных ситуаций, предлагая функционал фильтров, которых не существовало в языке C#.
Коды возврата vs. исключение
Стоит отдельно отметить, что можно вообще обойтись без исключений. Не выбрасывать их никогда, всегда работая на кодах возврата. Эта идея может показаться странной: мы ведь все привыкли к тому, что исключения есть, их много и их выбрасывают практически все классы нашей необъятной платформы. Однако, если не отворачиваться, а порассуждать, то можно прийти к выводам, что работа без исключений, возможно даже, более удобная и безопасная чем с ними.
Сами посудите: в случае исключения код будет разорван, прерван и эту поломку сможет перехватить только некий другой код выше во стеку. Причём влетая туда, в этот catch блок вы можете получить приложение, находящееся в не консистентном состоянии (например, если выброшено было не предусмотренное исключение, а блок перехватывал по типу базового класса). Плюс, если логика написана с использованием кодов ошибки, которые оформлены в виде enum , пользователь метода видит и возможность самой ошибки, а также может сразу понять, какие ошибки возможны (перейдя на определение enum ).
Если за основу взята модель с исключительными ситуациями, код возврата необходимо внедрять тогда, кода факт ошибки является нормой поведения. Например, в алгоритме парсинга текста ошибки в тексте являются нормой поведения, тогда как в алгоритме работы с разобранной строкой получение от парсера ошибки может являться критичным или, другими словами, чем-то исключительным.
Блоки Try-Catch-Finally коротко
Блок try создаёт секцию, от которой программист ожидает возникновения критических ситуаций, которые с точки зрения внешнего кода являются нормой поведения. Т.е. другими словами, если мы работаем с некоторым кодом, который в рамках своих правил считает внутреннее состояние более не консистентным и в связи с этим выбрасывает исключение, то внешняя система, которая обладает более широким видением той же ситуации возникшее исключение может перехватить блоком catch тем самым нормализовав исполнение кода приложения. А потому, перехватом исключений вы легализуете их наличие на данном участке кода. Это, на мой взгляд, очень важная мысль, которая обосновывает запрет на перехват всех типов исключений try-catch(Exception ex) < . >на всякий случай.
Это вовсе не означает, что перехватывать исключения идеологически плохо: я всего лишь хочу сказать о необходимости перехватывать то и только то, что вы ожидаете от конкретного участка кода и ничего больше. Например, вы не можете ожидать все типы исключений, которые наследуется от ArgumentException или же получение NullReferenceException поскольку это означает что проблема чаще всего не в вызываемом коде, а в вашем. Зато вполне корректно ожидать, что желаемый файл открыть вы не сможете. Даже если на 200% уверены, что сможете, не забудьте сделать проверку.
Третий блок — finally — также не должен нуждаться в представлении. Этот блок срабатывает для всех случаев работы блоков try-catch . Кроме некоторых достаточно редких особых ситуаций, этот блок отрабатывает всегда. Для чего введена такая гарантия исполнения? Для зачистки тех ресурсов и тех групп объектов, которые были выделены или же захвачены в блоке try и при этом являются зоной его ответственности.
Этот блок очень часто используется без блока catch , когда нам не важно, какая ошибка уронила алгоритм, но важно очистить все выделенные для этого конкретно алгоритма ресурсы. Простой пример: для алгоритма копирования файла необходимо: два открытых файла и участок памяти под кэш-буфер копирования. Память мы выделить смогли, один файл открыть смогли, а вот со вторым возникли какие-то проблемы. Чтобы запечатать все в одну «транзакцию», мы помещаем все три операции в единый try блок (как вариант реализации), с очисткой ресурсов — в finally . Пример может показаться упрощённым, но тут главное — показать суть.
Чего не хватает в языке программирования C#, так это блока fault , суть которого — срабатывать всегда, когда произошла любая ошибка. Т.е. тот же finally , только на стероидах. Если бы такое было, мы бы смогли, как классический пример делать единую точку входа в логирование исключительных ситуаций:
Также, о чем хотелось бы упомянуть во вводной части — это фильтры исключительных ситуаций. Для платформы .NET это новшеством не является, однако является таковым для разработчиков на языке программирования C#: фильтрация исключительных ситуаций появилась у нас только в шестой версии языка. Фильтры призваны нормализовать ситуацию, когда есть единый тип исключения, который объединяет в себе несколько видов ошибок. И в то время как мы хотим отрабатывать на конкретный сценарий, вынуждены перехватывать всю группу и фильтровать её — уже после перехвата. Я, конечно же, имею в виду код следующего вида:
Так вот теперь мы можем переписать этот код нормально:
И вопрос улучшения тут вовсе не в отсутствии конструкции switch . Новая конструкция как по мне лучше по нескольким пунктам:
- фильтруя по when мы перехватываем ровно то что хотим поймать и не более того. Это правильно идеологически;
- в новом виде код стал более читаем. Просматривая взглядом, мозг более легко находит определения ошибок, т.к. изначально он их ищет не в switch-case , а в catch ;
- и менее явное, но также очень важное: предварительное сравнение идёт ДО входа в catch блок. А это значит, что работа такой конструкции для случая промаха мимо всех условий будет идти намного быстрее чем switch с перевыбросом исключения.
Особенностью исполнения кода является то, что код фильтрации происходит до того как произойдёт развёртка стека. Это можно наблюдать в ситуациях, когда между местом выброса исключения и местом проверки на фильтрацию нет никаких других вызовов кроме обычных:

Как видно на изображении трассировка стека содержит не только первый вызов Main как место отлова исключительной ситуации, но и весь стек до точки выброса исключения плюс повторный вход в Main через некоторый неуправляемый код. Можно предположить, что этот код и есть код выброса исключений, который просто находится в стадии фильтрации и выбора конечного обработчика. Однако стоит отметить что не все вызовы позволяют работать без раскрутки стека. На мой скромный взгляд, внешняя унифицированность платформы порождает излишнее к ней доверие. Например, вызов методов между доменами с точки зрения кода выглядит абсолютно прозрачно. Тем не менее, работа вызовов методов происходит совсем по другим законам. О них мы и поговорим в следующей части.
Давайте начнём несколько издалека и посмотрим на результаты работы следующего кода (я добавил проброс вызова через границу между доменами приложения):
Если обратить внимание на размотку стека, то станет ясно, что в данном случае она происходит ещё до того, как мы попадаем в фильтр. Взглянем на скриншоты. Первый взят до того, как генерируется исключение:

А второй — после:

Изучим трассировку вызовов до и после попадания в фильтр исключений. Что же здесь происходит? Здесь мы видим, что разработчики платформы сделали некоторую с первого взгляда защиту дочернего домена. Трассировка обрезана по крайний метод в цепочке вызовов, после которого идёт переход в другой домен. Но на самом деле, как по мне так это выглядит несколько странно. Чтобы понять, почему так происходит, вспомним основное правило для типов, организующих взаимодействие между доменами. Эти типы должны наследовать MarshalByRefObject плюс — быть сериализуемыми. Однако, как бы ни был строг C#, типы исключений могут быть какими угодно. А что это значит? Это значит, что могут быть ситуации, когда исключительная ситуация внутри дочернего домена может привести к её перехвату в родительском домене. И если у объекта данных исключительной ситуации есть какие-либо опасные методы с точки зрения безопасности, они могут быть вызваны в родительском домене. Чтобы такого избежать, исключение сериализуется, проходит через границу доменов приложений и возникает вновь — с новым стеком. Давайте проверим эту стройную теорию:
В данном примере, для того чтобы выбросить исключение любого типа из C# кода (я не хочу никого мучить вставками на MSIL) был проделан трюк с приведением типа к не сопоставимому: чтобы мы бросили исключение любого типа, а транслятор C# думал бы, что мы используем тип Exception . Мы создаём экземпляр типа Program — гарантированно не сериализуемого и бросаем исключение с ним в виде полезной нагрузки. Хорошие новости заключаются в том, что вы получите обёртку над не-Exception исключениями RuntimeWrappedException , который внутри себя сохранит экземпляр нашего объекта типа Program и в C# перехватить такое исключение мы сможем. Однако есть и плохая новость, которая подтверждает наше предположение: вызов proxy.MethodInsideAppDomain(); приведёт к исключению SerializationException :

Т.е. проброс между доменами такого исключения не возможен, т.к. его нет возможности сериализовать. А это в свою очередь значит, что оборачивание вызовов методов, находящихся в других доменах фильтрами исключений все равно приведёт к развёртке стека несмотря на то что при FullTrust настройках дочернего домена сериализация казалось бы не нужна.
Стоит дополнительно обратить внимание на причину, по которой сериализация между доменами так необходима. В нашем синтетическом примере мы создаём дочерний домен, который не имеет никаких настроек. А это значит, что он работает в FullTrust. Т.е. CLR полностью доверяет его содержимому как себе и никаких дополнительных проверок делать не будет. Но как только вы выставите хоть одну настройку безопасности, полная доверенность пропадёт и CLR начнёт контролировать все что происходит внутри этого дочернего домена. Так вот когда домен полностью доверенный, сериализация по идее не нужна. Нам нет необходимости как-то защищаться, согласитесь. Но сериализация создана не только для защиты. Каждый домен грузит в себя все необходимые сборки по второму разу, создавая их копии. Тем самым создавая копии всех типов и всех таблиц виртуальных методов. Передавая объект по ссылке из домена в домен, вы получите, конечно, тот же объект. Но у него будут чужие таблицы виртуальных методов и как результат — этот объект не сможет быть приведён к другому типу. Другими словами, если вы создали экземпляр типа Boo , то получив его в другом домене приведение типа (Boo)boo не сработает. А сериализация и десериализация решает проблему: объект будет существовать одновременно в двух доменах. Там где он был создан — со всеми своими данными и в доменах использования — в виде прокси-объекта, обеспечивающего вызов методов оригинального объекта.
Передавая сериализуемый объект между доменами, вы получите в другом домене полную копию объекта из первого сохранив некоторую разграниченность по памяти. Разграниченность тоже мнимая. Она — только для тех типов, которые не находятся в Shared AppDomain . Т.е., например, если в качестве исключения бросить что-нибудь несериализуемое, но из Shared AppDomain , то ошибки сериализации не будет (можно попробовать вместо Program бросить Action ). Однако раскрутка стека при этом все равно произойдёт: оба случая должны работать стандартно. Чтобы никого не путать.
How to throw a C++ exception
I have a very poor understanding of exception handling(i.e., how to customize throw, try, catch statements for my own purposes).
For example, I have defined a function as follows: int compare(int a, int b)
I’d like the function to throw an exception with some message when either a or b is negative.
How should I approach this in the definition of the function?
![]()
5 Answers 5
The Standard Library comes with a nice collection of built-in exception objects you can throw. Keep in mind that you should always throw by value and catch by reference:
You can have multiple catch() statements after each try, so you can handle different exception types separately if you want.
You can also re-throw exceptions:
And to catch exceptions regardless of type:
Though this question is rather old and has already been answered, I just want to add a note on how to do proper exception handling in C++11:
Use std::nested_exception and std::throw_with_nested
It is described on StackOverflow here and here, how you can get a backtrace on your exceptions inside your code without need for a debugger or cumbersome logging, by simply writing a proper exception handler which will rethrow nested exceptions.
Since you can do this with any derived exception class, you can add a lot of information to such a backtrace! You may also take a look at my MWE on GitHub, where a backtrace would look something like this:
![]()
Just add throw where needed, and try block to the caller that handles the error. By convention you should only throw things that derive from std::exception , so include <stdexcept> first.
You could define a message to throw when a certain error occurs:
or you could define it like this:
Typically, you would have a try . catch block like this:
![]()
![]()
Adding to this answer, as it doesn’t seem advantageous to create another answer for this Q&A at this time.
In the case where you create your own custom exception, that derives from std::exception , when you catch "all possible" exceptions types, you should always start the catch clauses with the "most derived" exception type that may be caught. See the example (of what NOT to do):
NOTE:
The proper order should be vice-versa, i.e.- first you catch (const MyException& e) which is followed by catch (const std::exception& e) .
As you can see, when you run the program as is, the first catch clause will be executed (which is probably what you did NOT want in the first place).
Even though the type caught in the first catch clause is of type std::exception , the "proper" version of what() will be called — cause it is caught by reference (change at least the caught argument std::exception type to be by value — and you will experience the "object slicing" phenomena in action).
In case that the "some code due to the fact that XXX exception was thrown. " does important stuff WITH RESPECT to the exception type, there is misbehavior of your code here.
This is also relevant if the caught objects were "normal" object like: class Base<>; and class Derived : public Base <> .
g++ 7.3.0 on Ubuntu 18.04.1 produces a warning that indicates the mentioned issue:
In function ‘void illustrateDerivedExceptionCatch()’: item12Linux.cpp:48:2: warning: exception of type ‘MyException’ will be caught catch(const MyException& e) ^
item12Linux.cpp:43:2: warning: by earlier handler for ‘std::exception’ catch (const exception& e) ^
Again, I will say, that this answer is only to ADD to the other answers described here (I thought this point is worth mentioning, yet could not depict it within a comment).