こんにちは、鈴木です。
「例外安全 (Exception Safety)」という言葉をご存知でしょうか。
週末、本の整理をしていたときに「Exceptional C++」という本が出てきました。C++ のイディオム集といった感じの本なのですが、その中に登場する「例外安全」という言葉をご紹介します。
例外安全
「例外安全」とは、例外が発生したときに適切に処理されることを意味する言葉です。「適切に」とは、リソースリークが発生しないことや、オブジェクトの内部状態の整合性が保たれるということです。
「普段から意識してるよ」という方もいると思いますが、「聞いたことが無かった」という方は、例外安全という考え方を意識することで、今までより品質の良いコードを書けるようになるはずです。
Web上のリソースでは、以下のページに記載があります。
上記ページのタイトルを日本語にすると「汎用コンポーネントにおける例外安全性」です。C++ で汎用コンポーネントを作成するときの例外のハンドリングについての話です。そのときにポイントとなる考え方が「例外安全」です。
例外安全の保証レベル
例外安全には以下に挙げる保証レベルがあります。
- 基本的保証 (basic guarantee)
- 強い保証 (strong guarantee)
- 例外を投げない保証 (no-throw guarantee)
これらは包含関係にあり、「例外を投げない保証」を満たしていれば自動的に「強い保証」を満たします。また、「強い保証」を満たすのであれば「基本的保証」を満たします。
基本的保証 (basic guarantee)
基本的保証を満たすメソッドは、呼び出しによって例外が発生した場合に以下のことが保証されます。
- オブジェクトの内部状態の整合性が保たれる。
- オブジェクト内でリソースリークが発生しない。
強い保証 (strong guarantee)
強い保証を満たすメソッドは、メソッド内で例外が発生した場合に以下のことが保証されます。
- オブジェクトの内部状態は一切変更されない。
これはリレーショナルデータベースにおけるトランザクションと同じ性質です。
基本的保証は普通にコーディングしていても満たせることが多いですが、強い保証は意識しなければ満たせないことが多いです。
例えばメソッド内で副作用のある処理を行っている場合、意識してコーディングしなければ強い保証を満たせないことが多いでしょう。画面に印字した後に例外が発生した場合など、強い保証を満たすことができないケースもあります。
例外を投げない保証 (no-throw guarantee)
例外を投げない保証については説明不要かもしれませんが、これを満たすメソッドは以下のことを保証します。
- 決して例外を投げない。
これは四則演算しか行わないような一部のメソッドだけが満たすことのできる、最も強い保証レベルです。
例外安全への道
例として以下のコードを見てください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
class MyLogger def initialize(log_file_path) @file = File.open(log_file_path, 'a') @file.sync = true end def log(message) @file.puts message end def reopen(log_file_path) @file.close @file = File.open(log_file_path, 'a') # ファイルが開けなかったら? @file.sync = true end end |
ファイルに対してログ出力するクラスです。コンストラクタで出力先ファイルのパスを指定し、log メソッドでログ出力します。また、reopen メソッドで出力先ファイルを変更することができます。
「# ファイルが開けなかったら?」とコメントしている部分を見ていただきたいのですが、ここでファイルが開けずに例外が発生してしまうとどうなるでしょうか。
直前の行で @file.close してしまっているので、もう log メソッドでログを出力することができません。つまりオブジェクトの内部が整合性の取れていない状態となってしまいます。これは基本的保証を満たしていません。
これならどうでしょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
class MyLogger def initialize(log_file_path) @file = File.open(log_file_path, 'a') @file.sync = true end def log(message) @file.puts message end def reopen(log_file_path) # (1) 新しいファイルを先に開く. new_file = File.open(log_file_path, 'a') new_file.sync = true # (2) 開いていたファイルを閉じ, @file の値を変える. @file.close @file = new_file end end |
コメントに書いているように、新しいファイルを開いてから、インスタンス変数の @file を更新するように変更しました。
これで基本的保証が満たされるようになったかというと、・・・残念ながらそうではありません。@file.close で例外が発生する可能性があるからです。その場合、new_file が close されないため、リソースリークしてしまいます。
なかなか難しいですね。今度はどうでしょう。
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 |
class MyLogger def initialize(log_file_path) @file = File.open(log_file_path, 'a') @file.sync = true end def log(message) @file.puts message end def reopen(log_file_path) new_file = File.open(log_file_path, 'a') new_file.sync = true begin @file.close @file = new_file rescue new_file.close raise end end end |
今度は @file.close に失敗した場合に new_file.close するように変更しました。これでリソースリークは無くなりました。
コードを見ていただくと分かると思いますが、例外安全にするのって大変ですよね。嗅覚を働かせて「これはヤバイ」という部分を「begin ~ rescue ~ end」で囲っていくと。
これに対する答えとしては「オブジェクトを変更不能 (immutable) にせよ」であったり「内部状態を swap せよ」であったりします。これについてはまたの機会にご紹介したいと思います。
例外安全への道は意外と長いかもしれません。