forked from dlang/dlang.org
-
Notifications
You must be signed in to change notification settings - Fork 0
/
exception-safe.dd
456 lines (369 loc) · 11 KB
/
exception-safe.dd
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
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
Ddoc
$(D_S Exception Safe Programming,
Exception safe programming is programming so that if any piece of
code that might throw an exception does throw an exception, then
the state of the program is not corrupted and resources are not leaked.
Getting this right using traditional methods often results in complex,
unappealing and brittle code. As a result, exception safety often
is either buggy or simply ignored for
the sake of expediency.
<h3>Example</h3>
For example, if there's a Mutex m that must be acquired and held
for a few statements, then released:
---
void abc()
{
Mutex m = new Mutex;
lock(m); // lock the mutex
foo(); // do processing
unlock(m); // unlock the mutex
}
---
$(P If foo() throws an exception, then abc() exits via exception unwinding,
unlock(m) is never called and the Mutex is not released. This is a fatal
problem with this code.
)
$(P The RAII (Resource Acquisition Is Initialization) idiom
and the try-finally statement form the backbone of
the traditional approaches to writing exception safe programming.
)
$(P RAII is scoped destruction, and the example can be fixed by providing
a Lock class with a destructor that gets called upon the exit of the scope:
)
---
class Lock
{
Mutex m;
this(Mutex m)
{
this.m = m;
lock(m);
}
~this()
{
unlock(m);
}
}
void abc()
{
Mutex m = new Mutex;
scope L = new Lock(m);
foo(); // do processing
}
---
If abc() is exited normally or via an exception thrown from foo(), L gets
its destructor called and the mutex is unlocked.
The try-finally solution to the same problem looks like:
---
void abc()
{
Mutex m = new Mutex;
lock(m); // lock the mutex
try
{
foo(); // do processing
}
finally
{
unlock(m); // unlock the mutex
}
}
---
$(P Both solutions work, but both have drawbacks.
The RAII solution often requires
the creation of an extra dummy class, which is both a lot of lines of code to
write and a lot of clutter obscuring the control flow logic.
This is worthwhile to manage resources that must be cleaned up and that appear
more than once in a program, but it is clutter when it only needs to be
done once.
The try-finally
solution separates the unwinding code from the setup, and it can often be
a visually large separation. Closely related code should be grouped together.
)
$(P The $(LINK2 statement.html#ScopeGuardStatement, scope exit) statement is an easier
approach:
)
---
void abc()
{
Mutex m = new Mutex;
lock(m); // lock the mutex
scope(exit) unlock(m); // unlock on leaving the scope
foo(); // do processing
}
---
The $(D_KEYWORD scope)(exit) statement is executed at the closing curly
brace upon
normal execution, or when the scope is left due to an exception having
been thrown.
It places the unwinding code where it aesthetically belongs, next to the
creation of the state that needs unwinding. It's far less code to write
than either the RAII or try-finally solutions, and doesn't require the
creation of dummy classes.
<h3>Example</h3>
The next example is in a class of problems known as transaction processing:
---
Transaction abc()
{
Foo f;
Bar b;
f = dofoo();
b = dobar();
return Transaction(f, b);
}
---
$(P Both dofoo() and dobar() must succeed, or the transaction has failed.
If the transaction failed, the data must be restored to the state
where neither dofoo() nor dobar() have happened. To support that,
dofoo() has an unwind operation, dofoo_undo(Foo f) which will roll back
the creation of a Foo.
)
$(P With the RAII approach:
)
---
class FooX
{
Foo f;
bool commit;
this()
{
f = dofoo();
}
~this()
{
if (!commit)
dofoo_undo(f);
}
}
Transaction abc()
{
scope f = new FooX();
Bar b = dobar();
f.commit = true;
return Transaction(f.f, b);
}
---
With the try-finally approach:
---
Transaction abc()
{
Foo f;
Bar b;
f = dofoo();
try
{
b = dobar();
return Transaction(f, b);
}
catch (Object o)
{
dofoo_undo(f);
throw o;
}
}
---
$(P These work too, but have the same problems.
The RAII approach involves the creation of dummy classes, and the obtuseness
of moving some of the logic out of the abc() function.
The try-finally approach is wordy even with this simple example; try
writing it if there are more than two components of the transaction that
must succeed. It scales poorly.
)
$(P The $(D_KEYWORD scope)(failure) statement solution looks like:
)
---
Transaction abc()
{
Foo f;
Bar b;
f = dofoo();
scope(failure) dofoo_undo(f);
b = dobar();
return Transaction(f, b);
}
---
The dofoo_undo(f) only is executed if the scope is exited via an
exception. The unwinding code is minimal and kept aesthetically where
it belongs. It scales up in a natural way to more complex transactions:
---
Transaction abc()
{
Foo f;
Bar b;
Def d;
f = dofoo();
scope(failure) dofoo_undo(f);
b = dobar();
scope(failure) dobar_unwind(b);
d = dodef();
return Transaction(f, b, d);
}
---
<h3>Example</h3>
The next example involves temporarily changing the state of some object.
Suppose there's a class data member $(TT verbose), which controls the
emission of messages logging the activity of the class.
Inside one of the methods, $(TT verbose) needs to be turned off because
there's a loop that would otherwise cause a blizzard of messages to be output:
---
class Foo
{
bool verbose; // true means print messages, false means silence
...
bar()
{
auto verbose_save = verbose;
verbose = false;
... lots of code ...
verbose = verbose_save;
}
}
---
There's a problem if $(TT Foo.bar()) exits via an exception - the verbose
flag state is not restored.
That's easily fixed with $(D_KEYWORD scope)(exit):
---
class Foo
{
bool verbose; // true means print messages, false means silence
...
bar()
{
auto verbose_save = verbose;
verbose = false;
scope(exit) verbose = verbose_save;
...lots of code...
}
}
---
$(P It also neatly solves the problem if $(TT ...lots of code...) goes on at
some length, and in the future a maintenance programmer inserts a
return statement in it, not realizing that verbose must be reset upon
exit. The reset code is where it belongs conceptually, rather than where
it gets executed
(an analogous case is the continuation expression in a $(I ForStatement)).
It works whether the scope is exited by a return, break, goto, continue,
or exception.
)
$(P The RAII solution would be to try and capture the false state of verbose
as a resource, an abstraction that doesn't make much sense.
The try-finally solution requires arbitrarily large separation between
the conceptually linked set and reset code, besides requiring
the addition of an irrelevant scope.
)
<h3>Example</h3>
Here's another example of a multi-step transaction,
this time for an email program.
Sending an email consists of two operations:
$(OL
$(LI Perform the SMTP send operation.)
$(LI Copy the email to the $(DOUBLEQUOTE Sent) folder, which in POP is on the local
disk, and in IMAP is also remote.)
)
$(P Messages should not appear in $(DOUBLEQUOTE Sent) that haven't been actually sent,
and sent messages must actually appear in $(DOUBLEQUOTE Sent).
)
$(P Operation (1) is not undoable because it's a well-known distributed
computing issue. Operation (2) is undoable with some degree of
reliability. So we break the job down into three steps:
)
$(OL
$(LI Copy the message to $(DOUBLEQUOTE Sent) with a changed title $(DOUBLEQUOTE [Sending]
<Subject>). This operation ensures there's space in the client's IMAP
account (or on the local disk), the rights are proper, the connection
exists and works, etc.)
$(LI Send the message via SMTP.)
$(LI If sending fails, delete the message from $(DOUBLEQUOTE Sent). If the message
succeeds,
change its title from $(DOUBLEQUOTE [Sending] <Subject>) to $(DOUBLEQUOTE <Subject>).
Both of these operation have a high probability to succeed. If the
folder is local, the probability of success is very high. If the folder
is remote, probability is still vastly higher than that of step (1)
because it doesn't involve an arbitrarily large data transfer.)
)
---
class Mailer
{
void Send(Message msg)
{
{
char[] origTitle = msg.Title();
scope(exit) msg.SetTitle(origTitle);
msg.SetTitle("[Sending] " ~ origTitle);
Copy(msg, "Sent");
}
scope(success) SetTitle(msg.ID(), "Sent", msg.Title);
scope(failure) Remove(msg.ID(), "Sent");
SmtpSend(msg); // do the least reliable part last
}
}
---
This is a compelling solution to a complex problem.
Rewriting it with RAII would require two extra silly classes,
MessageTitleSaver and MessageRemover.
Rewriting the example with try-finally would require nested try-finally
statements or use of an extra variable to track state evolution.
<h3>Example</h3>
Consider giving feedback to the user about a lengthy
operation (mouse changes to an hourglass, window title is
red/italicized, ...).
With $(D_KEYWORD scope)(exit) that can be easily done without
needing to make an artificial resource out of whatever UI state element
used for the cues:
--------------
void LongFunction()
{
State save = UIElement.GetState();
scope(exit) UIElement.SetState(save);
...lots of code...
}
--------------
Even more so, $(D_KEYWORD scope)(success) and $(D_KEYWORD scope)(failure)
can be used to give an indication if the operation succeeded or if
an error occurred:
---
void LongFunction()
{
State save = UIElement.GetState();
scope(success) UIElement.SetState(save);
scope(failure) UIElement.SetState(Failed(save));
...lots of code...
}
---
<h2>When to use RAII, try-catch-finally, and Scope</h2>
RAII is for managing resources, which is different from managing state
or transactions. try-catch is still needed, as scope doesn't catch
exceptions. It's try-finally that becomes redundant.
<h2>Acknowledgements</h2>
$(P Andrei Alexandrescu argued about the usefulness of these constructs on the
Usenet and also defined
their semantics in terms of try/catch/finally in a series of posts
to comp.lang.c++.moderated under the title
$(LINK2 http://groups.google.com/group/comp.lang.c++.moderated/browse_frm/thread/60117e9c1cd1c510/b8cbe52786b0f506, A safer/better C++?)
starting Dec 6, 2005.
D implements the idea
with a slightly modified syntax following its creator's experiments with the
feature and useful
$(LINK2 http://www.digitalmars.com/d/archives/digitalmars/D/34277.html, suggestions)
from the D programmer community,
especially Dawid Ciezarkiewicz and Chris Miller.
)
$(P I am indebted to Scott Meyers for teaching
me about exception safe programming.
)
<h2>References:</h2>
$(OL
$(LI $(LINK2 http://www.cuj.com/documents/s=8000/cujcexp1812alexandr/alexandr.htm,
Generic<Programming>: Change the Way You Write Exception-Safe Code Forever)
by Andrei Alexandrescu and Petru Marginean
)
$(LI "Item 29: Strive for exception-safe code" in
$(LINK2 http://www.amazon.com/exec/obidos/ASIN/0321334876/classicempire,
Effective C++ Third Edition), pg. 127 by Scott Meyers
)
)
)
Macros:
TITLE=Exception Safety
WIKI=ExceptionSafe
CATEGORY_ARTICLES=$0