-
Notifications
You must be signed in to change notification settings - Fork 6
/
exceptions.tex
332 lines (276 loc) · 18.7 KB
/
exceptions.tex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
\section{Исключения}
\subsection{Введение}
Часто нехватка динамической памяти, неправильный ввод пользователя, ошибка с файловой системой, приводят к тому, что продолжение исполнения логики программы невозможно. Например, если наша функция foo вызывает malloc и malloc вернул ошибку, то функция foo должна завершиться и тоже вернуть ошибку. Возможно, что функция, вызывающая функцию foo, тоже проверит возвращаемое значение и завершится с ошибкой.
В C такая поведение реализовывалось, явной проверкой возвращаемого значения функции с помощью if и исполнением return'а в случае ошибки. C++ имеет встроенный в язык механизм поддержки такого поведения. Этот механизм называется механизмом исключений ({\it exceptions}).
Рассмотрим следующую функцию деления:
\begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++}
void div(int a, int b)
{
return a / b;
}
\end{minted}
Если в эту функцию передать в качестве $b$ $0$, то произойдет undefined behavior. Предположим, что мы хотим, чтобы функция сообщала об ошибке, когда $b = 0$. Для этого сначала необходимо объявить класс исключения, объекты которого будут хранить в себе информацию об исключении.
\begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++}
struct division_by_zero
{
division_by_zero(int dividend, string const &message)
: dividend(dividend), message(message)
{ }
private:
int dividend;
string message;
};
\end{minted}
Теперь можно переписать функцию $div$ следующим образом:
\begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++}
void div(int a, int b)
{
if (b == 0) // возникает исключительное состояние
throw division_by_zero(a, "in function div(int, int)"); // генерируем исключение.
return a / b;
}
\end{minted}
Оператор throw завершает исполнение текущей функции и возвращает ошибку в вызывающую функцию. Вызывающая сторона может обработать исключение следующим образом:
\begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++}
int main()
{
int n;
cin >> n;
try
{
// внутри try-блока указаны операторы, исключения в которых
// необходимо ловить
for (int i = 0, a, b; i < n; ++i)
{
cin >> a >> b;
cout << div(a, b);
}
}
catch(division_by_zero const& obj) // здесь указывается тип исключения,
// которое необходимо обработать
{
// код обработки исключения
cout << obj.dividend << "div by 0 " << obj.message();
}
}
\end{minted}
В данном примере, при завершении div с исключением, цикл for прерывается и исполняется catch-блок, который выводит сообщение об ошибке. После чего функция main завершается.
Если исключения не возникает, то цикл for отработает до конца, catch-блок вызван не будет.
\subsection{Описание конструкций}
Рассмотрим используемые конструкции подробнее. Блок \mintinline{c++}{try-catch} используется для обработки исключений и имеет общий вид:
\begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++}
try { /*операторы защищенного блока*/ }
// catch-блоки
catch(exception1 const& e) {/*код обработки*/}
catch(exception2 const& e) {/*код обработки*/}
...
catch(exceptionN const& e) {/*код обработки*/}
\end{minted}
Внутри блока try пишется код, исключения в котором необходимо ловить и обрабатывать. Если изнутри блока try вылетит исключение то, компилятор попытается подобрать подходящий catch-блок. В catch блоке указывается какие действия необходимо сделать, при возникновении исключения указанного типа.
Catch-блок может иметь две формы:
\begin{itemize}
\item
\mintinline{c++}{catch(/*declaration*/) { /*обработчик исключения*/ }} ловит исключение указанного или производных от него типов. Переменной исключения можно дать имя. Это позволяет обращаться к пойманному объекту исключения.
\item
\mintinline{c++}{catch(...) { /*обработчик исключения*/ }}
ловит исключения всех типов. В этом случае обращаться к объекту исключения невозможно.
\end{itemize}
Оператор \mintinline{c++}{throw} генерирует исключение. (Иногда говорят, "бросает"\ или "выбрасывает"\ исключение). При генерации исключения происходит следующее:
\begin{enumerate}
\item
Создается копия объекта переданного в оператор throw. Эта копия будет существовать до тех пор, пока исключение не будет обработано. Если тип объекта имеет конструктор копирования, то для создания копии будет использован конструктор копирования.
\item
Прерывается исполнение программы.
\item
Выполняется раскрутка стека, пока исключение не будет обработано.
\end{enumerate}
При раскрутке стека, вызываются деструкторы локальных переменных в обратном порядке их объявления. После разрушения всех локальных объектов текущей функции процесс продолжается в вызывающей функции. Раскрутка стека продолжается пока не будет найден try-catch-блок. При нахождении try-catch-блока, проверяется, может ли исключение быть обработано одним их catch-блоков.
\subsection{Как ловится исключение?}
Catch-блоки проверяются в том порядке, в котором написаны. Обработчик считается подходящим если:
\begin{enumerate}
\item
Тип, указанный в catch-блоке, совпадает с типом исключения или является ссылкой на этот тип.
\item
Класс, заданный в catch-блоке, является предком класса, заданного в throw, и наследование открытое (public).
\item
Указатель, заданный в операторе throw, может быть преобразован по стандартным правилам к указателю, заданному в catch-блоке.
\item
В catch-блоке указанно многоточие.
\end{enumerate}
Если найдет нужный catch-блок, то выполняется его код, остальные catch-блоки игнорируются, а выполнение продолжается после try...catch-блока и исключение считается обработанным. Если ни один catch-блок не подошел, процесс раскрутки стека продолжается.
\textcolor{red}{NB}) Так как поиск ведется последовательно, то нужно учитывать порядок catch-блоков (Например, catch(...) должен быть последним).
\textcolor{red}{NB}) Также при наследовании классов исключений следует различать catch(type\& obj) и catch(type obj). В первом случае obj ссылается на этот объект и копии не создается. Во втором случае при входе в catch блок делается копия объекта-исключения, вследствие чего мы теряем возможность вызывать виртуальные функции.
Пример:
\begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++}
struct base
{
virtual char const* msg() const
{
return "base";
}
};
struct derived : base
{
virtual char const* msg() const
{
return "derived";
}
};
void wrong()
{
try
{
throw derived();
}
catch (base e)
{
std::cout << e.msg() << std::endl;
}
}
void right()
{
try
{
throw Exception_derived();
}
catch (base const& e)
{
std::cout << e.msg() << std::endl;
}
}
\end{minted}
В данном примере функция right() выводит <<derived>>, а функция wrong() выводит <<base>>, поскольку внутри catch объект исключения был скопирован и новая копия имеет тип base.
В некоторых случаях внутри catch-блока может быть необходимо не завершать раскрутку стека. Для этого существует специальная форма оператора throw без аргумента. Она означает проброс текущего исключения с сохранением его динамического типа.
\begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++}
struct base
{
virtual char const* msg() const
{
return "base";
}
};
struct derived : base
{
virtual char const* msg() const
{
return "derived";
}
};
void third()
{
throw derived();
}
void second()
{
try
{
third();
}
catch (base const &obj)
{
std::cout << obj.msg() << std::endl;
throw; //пробрасываем
// здесь obj имеет тип Exception_base.
// но пробрасывается дальше исходное исключение имеющее тип Exception_derived.
}
}
void first()
{
try
{
second();
}
catch (derived const &obj)
{
std::cout << obj.msg() << std::endl;
}
}
int main()
{
first();
return 0;
}
\end{minted}
\textbf{Вывод программы:} \\
> derived \\
> derived \\
Статический тип объекта-параметра в текущем catch-блоке может отличаться от динамического типа исключения, но throw без аргументов пробрасывает текущее исключение сохраняя его динамический тип.
\textcolor{red}{NB})Если необходимо изменить тип исключения, то внутри catch-блока возможно кинуть исключение нового типа.
\begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++}
void load_config()
{
try
{
// ...
}
catch (file_not_found const&)
{
throw config_loading_failure();
}
}
\end{minted}
\subsection{Function-try-block}
Предположим есть класс, в конструкторе которого мы хотим ловить и обрабатывать исключения.
\begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++}
struct mytype
{
public:
mytype()
: member()
{
try
{
// Constructor's code
}
catch (...)
{
// ...
}
}
private:
member_type member;
}
\end{minted}
Заметим, что вызов конструкторов членов не находится внутри try-блока и исключения возникшие в их конструкторах не поймаются. Для ловли исключений из конструкторов членов существует специальный синтаксис называющийся {\it function-try-block}:
\begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++}
struct mytype
{
mytype()
try : member()
{
// Constructor's code
}
catch (...)
{
} // implicit throw
private:
member_type member;
}
\end{minted}
У функциональных try-блоков в конструкторах, есть особенность: они всегда бросают исключение повторно. Это связано с тем, что при ошибке создания члена, весь объемлющий объект оказывается не до конца созданным и продолжать работу невозможно.
Function-try-block может использоваться и с обычными функциями, в этом случае он эквивалентен оборащиванию тела в try...catch блок то есть он не пробрасывает исключение наверх автоматически.
\begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++}
int main()
try {
// main's body
}
catch (...)
{}
\end{minted}
\subsection{Уничтожение объекта при исключении в конструкторе}
При исполнении конструктора класса, вызываются конструкторы всех членов этого класса. Если исключение возникает при создании одного из членов класса, то в процессе раскрутки стека будут вызваны деструкторы от всех уже созданных членов. У самого объекта деструктор не вызывается, так как объект не считается созданным пока его конструктор не отработал полностью. Если конструктор захватывает некоторые ресурсы и потом бросается исключение, то конструктору следует самостоятельно освободить эти ресурсы перед выбрасыванием исключения.
\subsection{Исключения в деструкторах}
Как правило деструктор вызывается, чтобы произвести освобождение ресурсов и вызывающая сторона не заинтереснована в получении исключения. Начиная с C++11 деструкторы по умолчанию помечаются как \mintinline{c++}{noexcept}.
Теоретически из деструктора можно бросать исключения при этом нужно пометить его явно как \mintinline{c++}{noexcept(false)}. Иначе проброска исключения из \mintinline{c++}{noexcept} функции проведет в вызову \mintinline{c++}{std::terminate()} и завершению программы.
Следует так же иметь ввиду, что если деструктор был вызван не при штатном исполнении программы, а при раскрутке стека, то исключение вылетевшее из такого деструктора приведет к вызову \mintinline{c++}{std::terminate()}.
\subsection{\mintinline{c++}{std::terminate()}}
\mintinline{c++}{std::terminate()} --- это функция, которая вызывается, чтобы завершить программу, в случаях нерьезных ошибок использования исключений. Она вызывается:
\begin{itemize}
\item Если исключение брошено и не поймано ни одним catch-блоком, то есть пробрасывается наружу из main().
\item Если исключение пробрасывается наружу из созданного потока.
\item Если во время обработки исключения десктруктор, вызванный при раскрутке стека, бросает исключение и оно вылетает наружу.
\item Если функция переданная в \mintinline{c++}{std::atexit} и \mintinline{c++}{std::at_quick_exit} бросает исключение.
\item Если функция нарушает гарантии noexcept specification. Например, если функция помеченная как noexcept бросает исключение.
\item Если конструктор или деструктор статического или локального для потока объекта бросает исключение.
\end{itemize}
По умолчанию \mintinline{c++}{std::terminate()} вызывает \mintinline{c++}{std::abort()}, но можно это изменить, написав свою функцию и зарегистрировав ее с помощью функции \mintinline{c++}{std::set_terminate()}.