root/trunk/docsrc/exception-safe.dd

Revision 2040, 11.0 kB (checked in by walter, 1 year ago)

typography

  • Property svn:eol-style set to native
Line 
1 Ddoc
2
3 $(D_S Exception Safe Programming,
4
5 Exception safe programming is programming so that if any piece of
6 code that might throw an exception does throw an exception, then
7 the state of the program is not corrupted and resources are not leaked.
8 Getting this right using traditional methods often results in complex,
9 unappealing and brittle code. As a result, exception safety often
10 is either buggy or simply ignored for
11 the sake of expediency.
12
13
14 <h3>Example</h3>
15
16 For example, if there's a Mutex m that must be acquired and held
17 for a few statements, then released:
18
19 ---
20 void abc()
21 {
22     Mutex m = new Mutex;
23     lock(m);    // lock the mutex
24     foo();  // do processing
25     unlock(m);  // unlock the mutex
26 }
27 ---
28
29 $(P If foo() throws an exception, then abc() exits via exception unwinding,
30 unlock(m) is never called and the Mutex is not released. This is a fatal
31 problem with this code.
32 )
33
34 $(P The RAII (Resource Acquisition Is Initialization) idiom
35 and the try-finally statement form the backbone of
36 the traditional approaches to writing exception safe programming.
37 )
38
39 $(P RAII is scoped destruction, and the example can be fixed by providing
40 a Lock class with a destructor that gets called upon the exit of the scope:
41 )
42
43 ---
44 class Lock
45 {
46     Mutex m;
47
48     this(Mutex m)
49     {
50     this.m = m;
51     lock(m);
52     }
53
54     ~this()
55     {
56     unlock(m);
57     }
58 }
59
60 void abc()
61 {
62     Mutex m = new Mutex;
63     scope L = new Lock(m);
64     foo();  // do processing
65 }
66 ---
67
68 If abc() is exited normally or via an exception thrown from foo(), L gets
69 its destructor called and the mutex is unlocked.
70 The try-finally solution to the same problem looks like:
71
72 ---
73 void abc()
74 {
75     Mutex m = new Mutex;
76     lock(m);    // lock the mutex
77     try
78     {
79     foo();  // do processing
80     }
81     finally
82     {
83     unlock(m);  // unlock the mutex
84     }
85 }
86 ---
87
88 $(P Both solutions work, but both have drawbacks.
89 The RAII solution often requires
90 the creation of an extra dummy class, which is both a lot of lines of code to
91 write and a lot of clutter obscuring the control flow logic.
92 This is worthwhile to manage resources that must be cleaned up and that appear
93 more than once in a program, but it is clutter when it only needs to be
94 done once.
95 The try-finally
96 solution separates the unwinding code from the setup, and it can often be
97 a visually large separation. Closely related code should be grouped together.
98 )
99
100 $(P The $(LINK2 statement.html#ScopeGuardStatement, scope exit) statement is an easier
101 approach:
102 )
103
104 ---
105 void abc()
106 {
107     Mutex m = new Mutex;
108
109     lock(m);    // lock the mutex
110     scope(exit) unlock(m);  // unlock on leaving the scope
111
112     foo();  // do processing
113 }
114 ---
115
116 The $(D_KEYWORD scope)(exit) statement is executed at the closing curly
117 brace upon
118 normal execution, or when the scope is left due to an exception having
119 been thrown.
120 It places the unwinding code where it aesthetically belongs, next to the
121 creation of the state that needs unwinding. It's far less code to write
122 than either the RAII or try-finally solutions, and doesn't require the
123 creation of dummy classes.
124
125 <h3>Example</h3>
126
127 The next example is in a class of problems known as transaction processing:
128
129 ---
130 Transaction abc()
131 {
132     Foo f;
133     Bar b;
134
135     f = dofoo();
136     b = dobar();
137
138     return Transaction(f, b);
139 }
140 ---
141
142 $(P Both dofoo() and dobar() must succeed, or the transaction has failed.
143 If the transaction failed, the data must be restored to the state
144 where neither dofoo() nor dobar() have happened. To support that,
145 dofoo() has an unwind operation, dofoo_undo(Foo f) which will roll back
146 the creation of a Foo.
147 )
148
149 $(P With the RAII approach:
150 )
151
152 ---
153 class FooX
154 {
155     Foo f;
156     bool commit;
157
158     this()
159     {
160     f = dofoo();
161     }
162
163     ~this()
164     {
165     if (!commit)
166         dofoo_undo(f);
167     }
168 }
169
170 Transaction abc()
171 {
172     scope f = new FooX();
173     Bar b = dobar();
174     f.commit = true;
175     return Transaction(f.f, b);
176 }
177 ---
178
179 With the try-finally approach:
180
181 ---
182 Transaction abc()
183 {
184     Foo f;
185     Bar b;
186
187     f = dofoo();
188     try
189     {
190     b = dobar();
191     return Transaction(f, b);
192     }
193     catch (Object o)
194     {
195     dofoo_undo(f);
196     throw o;
197     }
198 }
199 ---
200
201 $(P These work too, but have the same problems.
202 The RAII approach involves the creation of dummy classes, and the obtuseness
203 of moving some of the logic out of the abc() function.
204 The try-finally approach is wordy even with this simple example; try
205 writing it if there are more than two components of the transaction that
206 must succeed. It scales poorly.
207 )
208
209 $(P The $(D_KEYWORD scope)(failure) statement solution looks like:
210 )
211
212 ---
213 Transaction abc()
214 {
215     Foo f;
216     Bar b;
217
218     f = dofoo();
219     scope(failure) dofoo_undo(f);
220
221     b = dobar();
222
223     return Transaction(f, b);
224 }
225 ---
226
227 The dofoo_undo(f) only is executed if the scope is exited via an
228 exception. The unwinding code is minimal and kept aesthetically where
229 it belongs. It scales up in a natural way to more complex transactions:
230
231
232 ---
233 Transaction abc()
234 {
235     Foo f;
236     Bar b;
237     Def d;
238
239     f = dofoo();
240     scope(failure) dofoo_undo(f);
241
242     b = dobar();
243     scope(failure) dobar_unwind(b);
244
245     d = dodef();
246
247     return Transaction(f, b, d);
248 }
249 ---
250
251 <h3>Example</h3>
252
253 The next example involves temporarily changing the state of some object.
254 Suppose there's a class data member $(TT verbose), which controls the
255 emission of messages logging the activity of the class.
256 Inside one of the methods, $(TT verbose) needs to be turned off because
257 there's a loop that would otherwise cause a blizzard of messages to be output:
258
259 ---
260 class Foo
261 {
262     bool verbose;   // true means print messages, false means silence
263     ...
264     bar()
265     {
266     auto verbose_save = verbose;
267     verbose = false;
268     ... lots of code ...
269     verbose = verbose_save;
270     }
271 }
272 ---
273
274 There's a problem if $(TT Foo.bar()) exits via an exception - the verbose
275 flag state is not restored.
276 That's easily fixed with $(D_KEYWORD scope)(exit):
277
278 ---
279 class Foo
280 {
281     bool verbose;   // true means print messages, false means silence
282     ...
283     bar()
284     {
285     auto verbose_save = verbose;
286     verbose = false;
287     scope(exit) verbose = verbose_save;
288
289     ...lots of code...
290     }
291 }
292 ---
293
294 $(P It also neatly solves the problem if $(TT ...lots of code...) goes on at
295 some length, and in the future a maintenance programmer inserts a
296 return statement in it, not realizing that verbose must be reset upon
297 exit. The reset code is where it belongs conceptually, rather than where
298 it gets executed
299 (an analogous case is the continuation expression in a $(I ForStatement)).
300 It works whether the scope is exited by a return, break, goto, continue,
301 or exception.
302 )
303
304 $(P The RAII solution would be to try and capture the false state of verbose
305 as a resource, an abstraction that doesn't make much sense.
306 The try-finally solution requires arbitrarily large separation between
307 the conceptually linked set and reset code, besides requiring
308 the addition of an irrelevant scope.
309 )
310
311 <h3>Example</h3>
312
313 Here's another example of a multi-step transaction,
314 this time for an email program.
315 Sending an email consists of two operations:
316
317 $(OL
318 $(LI Perform the SMTP send operation.)
319 $(LI Copy the email to the $(DOUBLEQUOTE Sent) folder, which in POP is on the local
320 disk, and in IMAP is also remote.)
321 )
322
323 $(P Messages should not appear in $(DOUBLEQUOTE Sent) that haven't been actually sent,
324 and sent messages must actually appear in $(DOUBLEQUOTE Sent).
325 )
326
327 $(P Operation (1) is not undoable because it's a well-known distributed
328 computing issue. Operation (2) is undoable with some degree of
329 reliability. So we break the job down into three steps:
330 )
331
332 $(OL
333 $(LI Copy the message to $(DOUBLEQUOTE Sent) with a changed title $(DOUBLEQUOTE [Sending]
334 &lt;Subject&gt;). This operation ensures there's space in the client's IMAP
335 account (or on the local disk), the rights are proper, the connection
336 exists and works, etc.)
337
338 $(LI Send the message via SMTP.)
339
340 $(LI If sending fails, delete the message from $(DOUBLEQUOTE Sent). If the message
341 succeeds,
342 change its title from $(DOUBLEQUOTE [Sending] &lt;Subject&gt;) to $(DOUBLEQUOTE &lt;Subject&gt;).
343 Both of these operation have a high probability to succeed. If the
344 folder is local, the probability of success is very high. If the folder
345 is remote, probability is still vastly higher than that of step (1)
346 because it doesn't involve an arbitrarily large data transfer.)
347
348 )
349
350 ---
351 class Mailer
352 {
353     void Send(Message msg)
354     {
355     {
356         char[] origTitle = msg.Title();
357         scope(exit) msg.SetTitle(origTitle);
358         msg.SetTitle("[Sending] " ~ origTitle);
359         Copy(msg, "Sent");
360     }
361     scope(success) SetTitle(msg.ID(), "Sent", msg.Title);
362     scope(failure) Remove(msg.ID(), "Sent");
363     SmtpSend(msg);  // do the least reliable part last
364     }
365 }
366 ---
367
368 This is a compelling solution to a complex problem.
369 Rewriting it with RAII would require two extra silly classes,
370 MessageTitleSaver and MessageRemover.
371 Rewriting the example with try-finally would require nested try-finally
372 statements or use of an extra variable to track state evolution.
373
374 <h3>Example</h3>
375
376 Consider giving feedback to the user about a lengthy
377 operation (mouse changes to an hourglass, window title is
378 red/italicized, ...).
379 With $(D_KEYWORD scope)(exit) that can be easily done without
380 needing to make an artificial resource out of whatever UI state element
381 used for the cues:
382
383 --------------
384 void LongFunction()
385 {
386     State save = UIElement.GetState();
387     scope(exit) UIElement.SetState(save);
388     ...lots of code...
389 }
390 --------------
391
392 Even more so, $(D_KEYWORD scope)(success) and $(D_KEYWORD scope)(failure)
393 can be used to give an indication if the operation succeeded or if
394 an error occurred:
395
396 ---
397 void LongFunction()
398 {
399     State save = UIElement.GetState();
400     scope(success) UIElement.SetState(save);
401     scope(failure) UIElement.SetState(Failed(save));
402     ...lots of code...
403 }
404 ---
405
406 <h2>When to use RAII, try-catch-finally, and Scope</h2>
407
408 RAII is for managing resources, which is different from managing state
409 or transactions. try-catch is still needed, as scope doesn't catch
410 exceptions. It's try-finally that becomes redundant.
411
412 <h2>Acknowledgements</h2>
413
414 $(P Andrei Alexandrescu argued about the usefulness of these constructs on the
415 Usenet and also defined
416 their semantics in terms of try/catch/finally in a series of posts
417 to comp.lang.c++.moderated under the title
418 $(LINK2 http://groups.google.com/group/comp.lang.c++.moderated/browse_frm/thread/60117e9c1cd1c510/b8cbe52786b0f506, A safer/better C++?)
419 starting Dec 6, 2005.
420 D implements the idea
421 with a slightly modified syntax following its creator's experiments with the
422 feature and useful
423 $(LINK2 http://www.digitalmars.com/d/archives/digitalmars/D/34277.html, suggestions)
424 from the D programmer community,
425 especially Dawid Ciezarkiewicz and Chris Miller.
426 )
427
428 $(P I am indebted to Scott Meyers for teaching
429 me about exception safe programming.
430 )
431
432 <h2>References:</h2>
433
434 $(OL
435
436 $(LI $(LINK2 http://www.cuj.com/documents/s=8000/cujcexp1812alexandr/alexandr.htm,
437 Generic&lt;Programming&gt;: Change the Way You Write Exception-Safe Code Forever)
438 by Andrei Alexandrescu and Petru Marginean
439
440 )
441
442 $(LI "Item 29: Strive for exception-safe code" in
443 $(LINK2 http://www.amazon.com/exec/obidos/ASIN/0321334876/classicempire,
444  Effective C++ Third Edition), pg. 127 by Scott Meyers
445
446 )
447
448 )
449
450 )
451
452 Macros:
453     TITLE=Exception Safety
454     WIKI=ExceptionSafe
Note: See TracBrowser for help on using the browser.