forked from dlang/dlang.org
-
Notifications
You must be signed in to change notification settings - Fork 4
/
exception-safe.dd
455 lines (368 loc) · 13.6 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
Ddoc
$(D_S 例外安全なプログラミング,
例外安全なプログラミングとは、
例外を投げる可能性があるコードが実際に例外を投げた場合に、
プログラムの状態が壊れずリソースもリークしないように作るプログラミングのことを言います。
これを正しく実現するには、既存の方法では、複雑で読みにくく脆いコード
を書かねばなりませんでした。結果として、例外安全性に関して
バグが残っていることが非常に多かったり、そもそも手間を省くために
例外安全が完全に無視されたりしてきました。
<h3>例</h3>
例として、数行の文を実行するあいだMutexをロックして、
終わったら解放するというケースを考えてみましょう:
---
void abc()
{
Mutex m = new Mutex;
lock(m); // mutexをロック
foo(); // 処理を行う
unlock(m); // mutexをアンロック
}
---
$(P >foo() が例外を投げると、abc() は例外による巻き戻しで終了します。
この場合 unlock(m) は呼び出されず、Mutexは解放されません。
これはこのコードの致命的な欠陥です。
)
$(P RAII (Resource Acquisition Is Initialization) イディオムと、
try-finally文。この二つが、例外安全なプログラミングを実現するための
これまでの基本的な方法でした。
)
$(P RAII とはスコープに関連づけられた破棄処理のことで、先ほどの例は、
スコープの終了時に呼び出されるデストラクタをもったLockクラスを使うことで修正できます:
)
---
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(); // 処理を行う
}
---
abc() が正常終了しても foo() からの例外で終了しても、L のデストラクタは呼び出され、
Mutex は解放されます。
また、同じ問題を try-finally で解決すると次のようになります:
---
void abc()
{
Mutex m = new Mutex;
lock(m); // mutexをロック
try
{
foo(); // 処理を行う
}
finally
{
unlock(m); // mutexをアンロック
}
}
---
$(P どちらの方法も問題を解決はします。しかし、どちらにも欠点があります。
RAII による方法では
余分なダミークラスを作る必要が生じ、コード量も増えますし、
実行フローの流れも見通しが悪くなります。
これは、必ず解放されなければならないリソースを、プログラム中で何度も使う場合には
問題になりません。しかし、
一度だけ必要な解法処理を記述するには面倒です。
try-finally による解決は、
リソースの初期化コードと巻き戻し処理のコードが分離して記述され、
実際ソースコード上で視覚的に大きく離れてしまします。しかし、密接に関連した処理は一カ所にまとまっているべきです。)
$(P そこで $(LINK2 statement.html#ScopeGuardStatement, scope exit) 文が、
もっと簡単なアプローチを可能にします:
)
---
void abc()
{
Mutex m = new Mutex;
lock(m); // mutexをロック
scope(exit) unlock(m); // スコープ終了時にアンロック
foo(); // 処理を行う
}
---
$(D_KEYWORD scope)(exit) 文は、
正常な実行で中括弧を抜ける時か、
あるいは例外が投げられてスコープを抜けるときに
実行されます。
この方法では、巻き戻しコードが戻す状態の生成部分の
すぐ隣に配置されるという綺麗なソースになります。また、
RAII と比べても try-finally と比べてもコード量も少なく、
ダミークラスを定義する必要もありません。
<h3>例</h3>
次の例は、トランザクション処理と呼ばれる種類の問題です:
---
Transaction abc()
{
Foo f;
Bar b;
f = dofoo();
b = dobar();
return Transaction(f, b);
}
---
$(P dofoo() と dobar() の両方が成功するか、そうでなければトランザクション失敗とします。
トランザクションが失敗する場合は、
dofoo() も dobar() も実行される前の状態にデータが戻っていないといけません。
これを実現するために、dofoo() の操作を巻き戻す dofoo_undo(Foo f) があって、
Fooの生成を取り消せるようになっています。
)
$(P さて、RAIIの方法では:
)
---
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);
}
---
try-finally の方法では:
---
Transaction abc()
{
Foo f;
Bar b;
f = dofoo();
try
{
b = dobar();
return Transaction(f, b);
}
catch (Object o)
{
dofoo_undo(f);
throw o;
}
}
---
$(P どちらも動作はしますが、やはり同じ問題を抱えています。
RAIIではダミークラスを作る必要があり、abc() 関数のロジックを一部外に
取り出したことで読みにくくなっています。
try-finally の方法はこのシンプルな例ではまだなんとかなっているようにも見えますが、
3個以上の処理を含むトランザクションを書こうとすると、
拡張性に乏しいことがわかります。
)
$(P $(D_KEYWORD scope)(failure) 文による解決法では次のようになります:
)
---
Transaction abc()
{
Foo f;
Bar b;
f = dofoo();
scope(failure) dofoo_undo(f);
b = dobar();
return Transaction(f, b);
}
---
dofoo_undo(f) はスコープが例外によって終了するときにのみ実行されます。
巻き戻しコードは最小限で、あるべき場所に綺麗に配置されています。
もっと複雑なトランザクションの場合でも、拡張のしかたは明らかです:
---
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>例</h3>
次は、あるオブジェクトの例を一時的に変更するという例です。
クラスのデータメンバ $(D verbose) があって、
クラスの動作のログ取り動作を制御しているものとしましょう。
メソッドの一つが、物凄く大量のメッセージをはき出すループを含んでいて大変なことに
なるので $(D verbose) をオフにする必要があったとします:
---
class Foo
{
bool verbose; // trueならメッセージを表示。falseなら表示しない
...
bar()
{
auto verbose_save = verbose;
verbose = false;
... lots of code ...
verbose = verbose_save;
}
}
---
$(D Foo.bar()) が例外で終了すると問題が起きます。
verbose の状態が復元されません。
この問題は、$(D_KEYWORD scope)(exit) を使うと簡単に解決します:
---
class Foo
{
bool verbose; // trueならメッセージを表示。falseなら表示しない
...
bar()
{
auto verbose_save = verbose;
verbose = false;
scope(exit) verbose = verbose_save;
... ながーいコード ...
}
}
---
$(P これは、将来的に $(D ...ながーいコード...) の中に、保守プログラマが
verboseを戻す必要に気づかずreturn文を挿入してしまった、
という場合でも問題なく動くコードになっています。
復元コードが、
実行される箇所ではなくそれが概念的に属する場所に配置されているからです。
これは $(I ForStatement) の条件更新式の利点に似ています。
上のコードは、スコープから return, break, goto, continue, 例外の
どれで抜ける場合も正しく動作します。
)
$(P RAII で解決しようとすると、verbose の状態をリソースとして捕捉することになり、
意味のある抽象化になりません。
try-finally による方法は、
概念的に関連している値のセットと復元コードが、
間に関係のないコードが挟まることでいくらでも離されてしまう危険性を含んでいます。
)
<h3>例</h3>
複数ステップのトランザクションの別の例を挙げましょう。
今度は、emailプログラムを例にとります。
emailの送信は、二つの操作からなっています:
$(OL
$(LI SMTP の送信操作の実行)
$(LI 送信済みメールの $(DOUBLEQUOTE Sent) フォルダへのコピー。POPならばローカルディスクへ、
IMAPならリモートに保存することになります)
)
$(P 実際の送信が完了していないメールが $(DOUBLEQUOTE Sent) の中にあってはいけませんし、
送信済みのメールは確実に $(DOUBLEQUOTE Sent) に収まっている必要があります。
)
$(P 操作1は、ご存じの通りコンピュータ間の分散処理であるため、取り消すことはできません。
操作2は、ある程度の確実性をもって取り消すことができます。
そこで、作業を3つのステップに分解して考えます:
)
$(OL
$(LI メールを、タイトルを $(DOUBLEQUOTE [Sending] <Subject>) に変更しつつ
$(DOUBLEQUOTE Sent) に保存。 この操作は、クライアントのIMAPアカウント(あるいはローカルディスク)
に空き容量があり、権限が適切に設定されていて、ネットワーク接続が正しく保たれている
ことなどなど、が必要になります。)
$(LI SMTP経由でメッセージを送信)
$(LI 送信が失敗した場合、メッセージを $(DOUBLEQUOTE Sent) から削除します。
送信が成功した場合は、
タイトルを $(DOUBLEQUOTE [Sending] <Subject>) から $(DOUBLEQUOTE <Subject>) に変更します。
どちらの操作も、ほぼ確実に成功すると考えられます。フォルダがローカルの場合は
特に確実です。フォルダがリモートの場合も、
操作(1)と比べると、
巨大なデータ転送を伴わない分、成功確率はずっと高いと言えるでしょう。)
)
---
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); // 最後にもっとも信頼性の低い処理
}
}
---
このコードは以上の複雑な問題に対するなかなか悪くない解決策になっています。
RAIIで書き直そうと思うと、二つの余分でまぬけなクラス、
MessageTitleSaver と MessageRemover が必要になります。
try-finally で書き直そうと思うと、try-finally文のネストや、
状態の進展を記録する余分な変数が必要になります。
<h3>例</h3>
時間のかかる処理の実行中であることをユーザーにフィードバックする
(マウスカーソルを砂時計に変える、Windowタイトルを
赤くしたり斜体にしたりする、...) という例を考えます。
$(D_KEYWORD scope)(exit) を使えば、
使用されるUI状態を管理する人工的なリソースオブジェクトを必要とせず、
簡単に実現できます:
--------------
void LongFunction()
{
State save = UIElement.GetState();
scope(exit) UIElement.SetState(save);
...ながーいコード...
}
--------------
さらに、$(D_KEYWORD scope)(success) と $(D_KEYWORD scope)(failure)
を使えば操作が成功したのか失敗したのかのフィードバックを返すのも
簡単です:
---
void LongFunction()
{
State save = UIElement.GetState();
scope(success) UIElement.SetState(save);
scope(failure) UIElement.SetState(Failed(save));
...ながーいコード...
}
---
<h2>RAII, try-catch-finally, scope をそれぞれ使うべき場合</h2>
RAII は、状態やトランザクション管理ではなく、リソース管理に威力を発揮する機能です。
scope文だけでは例外のcatchはできませんから、try-catch も必要です。
try-finally は、scope文によって冗長な構文となったと言えます。
<h2>謝辞</h2>
$(P Andrei Alexandrescu はここで紹介した要素の有用性について
Usenet で議論し、
またDec 6, 2005から始まる
comp.lang.c++.moderated への一連の投稿
$(LINK2 http://groups.google.com/group/comp.lang.c++.moderated/browse_frm/thread/60117e9c1cd1c510/b8cbe52786b0f506, A safer/better C++?)
にて、その意味論を try/catch/finally を用いて定義しました。
D ではこのアイデアを、
考案者の実験に沿って構文を多少変更し、
また D プログラマコミュニティから…特にDawid Ciezarkiewicz と Chris Miller からの重要な
$(LINK2 http://www.digitalmars.com/d/archives/digitalmars/D/34277.html, 提案)
を元に、
実装しました。
)
$(P また、
Scott Meyers には例外安全なプログラミングについてご教授いただきました。感謝します。
)
<h2>参考文献:</h2>
$(OL
$(LI $(LINK2 http://drdobbs.com/184403758,
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=例外安全
WIKI=ExceptionSafe
CATEGORY_ARTICLES=$0