Edited at
例外Day 2

例外安全と例外中立

More than 3 years have passed since last update.


現代のC++で例外安全問題を抜きにして、障害に強い強固なコードを書くことはほとんど不可能に近い。以上。 Hurb Sutter [1]


例外処理における目的は、例外の回復と例外の通知の大きく2つあります。残念ながら例外の回復はとても難しく、場合によってはそもそも不可能だったりします。その場合、例外が発生したことをより上位のレイヤーに通知する事で例外処理を託します。この時、例外の通知を受け取った側は何を前提に例外の回復を行えばよいでしょうか。例外の発生によってデータ整合性は崩れてしまっているかもしれません。通知を受け取った上位レイヤーはあらゆる状態を想定して例外の回復を試みなければならないのでしょうか。もしそうだとすれば、ただでさえ難しい例外の回復がいよいよもって現実的ではなくなってしまいます。

明らかに上位レイヤーが持つべき前提条件が存在します。これは例外を通知する側が満たすべき保証と言い換えることもできます。最も強い、そして可能であれば満たすべき保証は、例外が(もっと言えば例外的状況が)発生する前の状態にロールバックするものです。この保証は、例外の発生による副次的な操作、つまり副作用が全くないという前提条件を例外を通知した上位レイヤーにもたらすことができます。

しかし、この保証はデータ間の整合性などといった横断的でコントロールしづらい要求を含むため、これを満たす事は簡単ではありません。そこで、少し緩めた保証として、単独のデータが矛盾なく利用可能である事を考えます。この保証はある意味では当然の事を保証しているだけです。単独のデータが矛盾し、利用できなくなってしまう時、そしてそのデータが十分に抽象化されている時、我々にはもう手の施しようがありません。


例外安全

例外処理を効果的に行うために、例外を通知する側が適切な保証を満たす必要があること、その保証にはいくつかの段階があることを述べました。Dave Abrahams氏とHurb Sutter氏は例外処理における保証を例外安全と命名し、保証の段階として以下の3つを挙げています[2]*1。


  1. 基本保証

  2. 強い保証

  3. no-fail保証


基本保証

基本保証は単独のデータが矛盾なく利用可能であることの保証です。基本保証は名前の通り最も基本的な保証段階であり、3つの保証の中で最も緩やかな保証です。そのため、基本保証は原則として満たされていることが期待されます。

Sutter氏は著書の『Exceptional C++』で基本保証を紹介しました。書籍の名前から伺えるようにC++でのプログラミングを前提にしているため、ここで紹介するためにはいくつかの一般化が必要です。『Exceptional C++』では基本保証の例として「例外が発生しても使用したオブジェクトはメモリリークを発生しないこと」を挙げています。しかし、いくつかの言語においてこれはプログラマではなくプログラミング言語に責任があります。とはいえGC機構がある言語でもファイル等のリソースはしばしば明示的な開放が要求されます。基本保証を満たすと言った時、上位レイヤーが関連リソースにアクセス出来ない場合は関連リソースを開放する責任が発生するでしょう。


強い保証

強い保証は操作のロールバックを行う保証です。例外が発生した時、例外の発生前後での操作がアトミックではない場合はしばしば関係間での整合性が崩れてしまう場合があります。強い保証はこのような整合性問題を発生しないことを保証します。

強い保証をより簡潔に「例外が発生しても副作用をもたらさない保証」と言い換えることができます。これは操作が純粋でない場合には特に達成が難しくなってしまいます。また、強い保証は例外回復を行う上で必須の保証です。上位レイヤーにとって下位レイヤーがもたらした例外による副作用を正確に予測する事はほとんど不可能です。もしそれが可能であれば、そのレイヤー間は密結合しているという事になるからです。つまり、基本保証の通知は例外回復をもたらしません。例外を回復したい場合、例外発生元が強い保証を満たすことが最低条件です。


no-fail保証

例外処理という観点から操作と例外の関係を見た時最も強力な保証とは、必ず例外が発生しない保証です。no-fail保証は必ず失敗する「バグ」の真逆、処理が必ず成功する事を保証するものです。

ここで例外的状況の定義を思い出してみてください。「ある命名された処理が、その名前から期待される処理を完了できなかった時」こそが例外的状況でした。その場合、例外的状況を決定している要因は処理に命名された名前です。この名前、より一般化して「処理の仕様」こそが例外的状況を決定しています。「例外安全は決して「単なる実装の詳細」ではない [1]」のです。

つまり、例外と仕様は不可分であり、例外設計は仕様に強く依存します。であれば、例外を発生しない保証は技術的な困難さよりも、仕様的、本質的困難さが大きなウエイトを占めます。そもそもno-fail保証を満たすことが出来ない処理も存在します。大抵の場合、IO系APIが絡むとno-fail保証は満たすことができません。

さて、強い保証が満たせていればno-fail保証が満たされるべき理由などあるのでしょうか。これは実のところ例外機構やプログラミング言語による要請が主な原因です。例えば、GCのあるプログラミング言語の多くはファイナライゼーション時(GCがオブジェクトを開放する際に呼ばれる任意の処理)に例外を発生すると致命的なパフォーマンス悪化や、最悪ソフトウェアがダウンしてしまいます。『Exceptional C++』の場合ですと、C++のデストラクタ等をno-fail保証を導入する理由に挙げています。ですので、言語や例外機構によってはno-fail保証は必要ではありません。


広義の例外安全

Sutter氏はこのうちいずれかの保証を満たすプログラムを例外安全であると述べています。しかし、基本保証が最も弱い保証であることから、ここでは基本保証を満たすプログラムを広義の例外安全であると言うことにしましょう。

3つの保証は完全な包括関係にあります。no-fail保証が満たせれば、強い保証や基本保証も自動的に満たされます。強い保証が満たせれば、基本保証は自動的に満たされます。であれば、基本保証さえ満たしていれば、いずれかの例外安全性を満たしていると言い換えることができるのです。

つまり、基本保証が満たされていない処理は例外安全ではありません。例外安全でなければ個々のデータを安全に利用できると言った基本的な前提さえ持つことが出来ません。これでは何の処置のしようもありませんので、ソフトウェアを強制終了させる事と変わりがなくなってしまいます。

以上から言えることとして、強い保証やno-fail保証が満たされない、満たすことが出来ないことはありえますが、基本保証が満たされないことはありえません。例外の通知を受けた上位レイヤーは基本保証が満たされていること、つまり例外安全であることを前提とすべきであり、全ての処理は例外安全を満たす責任があります。

では、何らかの理由で例外安全が満たせない場合はどうすれば良いのでしょう。これは、利用しているライブラリ等が例外安全でない場合などで起こりうることです。その場合はソフトウェアを速やかに強制終了すべきです。基本保証さえ満たせないソフトウェアは、ユーザーの要求を果たす能力をすでに失ってしまっているからです。

例外安全と保証の強度.png

例外安全についての締めくくりとして、各々の安全性が何を保証するためのものなのかを表としてまとめました。

例外安全
目的

基本保証
安全なソフトウェアを作るため

強い保証
例外回復を行うため

no-fail保証
言語や機構に要求されるため

安全なソフトウェアとは、平たく言えば失敗しても暴走したり、データを破壊してしまわないようなものを指します。つまり、広義の例外安全を満たせたとしてもその程度の保証しか出来ず、例外回復を行えません。例外回復は強い保証以上の例外安全が満たされている必要があります。


例外中立

セクションの結論部分の文章が不適切だったため、修正しました。

例外安全によって例外を通知する側の責任が明確になりました。しかし、そもそも例外的状況で例外を通知するかどうかという判断についてはどうでしょう。全ての処理は例外的状況下で上位レイヤーに例外を通知する責任があるのでしょうか。

Sutter氏は例外の通知に関する責務として、以下の行為を定め、例外中立と命名しました。


関数で例外を処理(または解釈したり意図的に吸収したり)しない場合、その例外を処理できる呼び出し側に伝えられるようにすること。 [3]


より一般化すると、例外中立とは「例外的状況下で例外回復が行えなかった場合は例外を上位レイヤーに通知する責任」のことです。Sutter氏は全ての処理は例外中立でなければならないと述べています。いわゆる例外の握りつぶし行為は例外中立を阻害します。

では何故例外中立は重要なのでしょうか。例外中立性が保たれていなければ、例外を通知するすべがないのでその例外を回復させる手立てがありません。それどころか、ソフトウェアが問題を抱えていることさえ認識できません。

広義の例外安全は安全なソフトウェアを開発する上で必須でした。例外中立は強い保証を支援するために、そして例外回復を行う上で必須です。いくら個々の処理が頑張って強い保証を満たしても、肝心の例外が握りつぶされては回復のしようがないためです。

例外中立を満たすことは難しいことではありません。ただ、例外を握りつぶさなければ良いだけだからです。そして、強い保証を台無しにしないためにも、全ての処理は例外中立であるべきです。まとめましょう。全ての処理は例外安全かつ例外中立である事が求められます


それで十分なのか

では全ての処理が例外安全かつ例外中立であればそれで十分なのでしょうか。今のモデルでは致命的な問題がいくつか存在しています。ここではその問題を提起するだけに留めます。後日述べるいくつかの例外機構は、ここで述べたいくつかの問題に何らかのアプローチを図っています。


静的例外安全

以下の文章について誤りがあったため大幅に修正しました。

例外安全が達成できたとして、ある処理がどの程度例外安全であるかは依然として不明です。全ての処理が例外安全であるとは、簡単にいえば「意図しないようなソフトウェアの暴走を防ぐ」程度の意味合いでしかありません。もっと言えば、基本保証は現代的な多くのプログラミング言語ではほとんど自動的に達成されます。

例外回復を視野に入れた場合、全ての処理の例外安全の程度を事前に知る必要があります。そして、強い保証やno-fail保証が達成されている処理のみで完結している場合のみ、例外回復が現実的なものとなります。繰り返しになりますが、基本保証が満たされただけでは例外回復は行えません。データを横断するような副作用を事前に予測することが事実上不可能だからです。もし基本保証しか保証しない処理を用いて例外回復に成功したとしても、それはその処理での副作用が偶然にも例外回復とは関係のない部分でしかなかったという幸運に過ぎません。

つまり、実行前に例外安全の程度を知るための機構、静的例外安全が必要になります。言語によっては基本保証やno-fail保証を静的に解析可能なものがあります。いくつかの言語ではno-fail保証をそのまま言語レベルでサポートしています。コードを静的に解析することでno-fail保証を約束するものもあります。また、基本保証を強制している言語や機構があります。それらの言語等では、全ての処理は基本保証を満たしている事は解析するまでもなく明らかです。しかし、強い保証については少なくとも筆者が知りうる限り静的に解析可能である言語や機構を知りません。前述の通り、全ての処理がno-fail保証を満たせるわけではないので、これでは不十分です。であるならば、現在のプログラミング言語を用いた例外回復は、全て偶然と運に依存していることになります。回復できればラッキー程度でしかありません。


静的例外中立

以下の文章について誤りがあったため大幅に修正しました。

例外安全を事前に知ることが重要なように、例外中立も事前に知ることは安全なソフトウェアの構築においてとても重要です。静的例外中立は不適切な例外の握りつぶしを実行前に検査することが可能な機構と考えることができます。

もし静的例外中立がサポートされていない場合、いくら例外安全を、特に強い保証を満たしていても意味がありません。例外が適切に通知されなければ回復のしようがないからです。


例外設計の変更コスト

ここまでで例外と仕様は不可分なものであると述べました。では、仕様が変更された場合、発生しうる例外や、そもそもその処理から例外が発生するかどうかさえも変わってしまいます。不幸なこととして、例外の変更を正確に予測することは極めて難しく、その変更に追従するコストはしばしば現実的ではなくなります。ある処理が結果的に同じ効果をもたらしても、内部的な処理方法が違えば例外の観点から2つの処理は同じとはいえません。これを少し小難しい言葉を使うと「型や値の観点から言えば処理の等しさは外延的に決定できるが、例外の観点から言えば処理の等しさは内包的に決定される」と言い換えることができます。

例えば前述のように例外安全や例外中立を事前に知ることが出来たとしても、内部処理が変わればその結果も変わります。とすれば、その処理のための例外処理も変わります。さらに言えば、その処理を呼び出している別の処理の例外安全や例外中立にも影響を与えます。全ての変更が連鎖的に起こってしまうので、そのままでは複雑過ぎて使い物になりません。

何故このような不幸が起きたのでしょうか。一言で言えば例外はあまりにも具象的だからです。特に例外回復を視野に入れた場合、ある処理と直接対応する専用の例外表現が必要になります。そのため抽象化や共通化が行えず、保守性や再利用性を著しく低下させてしまいます。

例外安全と例外中立の議論は、例外回復のための十分具体的な情報と、特定処理に依存しない抽象性を併せ持った例外機構の存在の必要性を浮き彫りにしました。しかし、両者の特性を併せ持つ例外機構は少なくとも筆者は未だ知りません。


注釈

*1: 経緯としてはDave Abrahams氏が『STLport: Exception Handling』[4]にて問題化し、Hurb Sutter氏が『Exeptional C++』等で紹介した形になります*2。

*2: 当初『STLport: Exception Handling』ではno-fail保証に言及が無いと書いていましたが、no-fail保証に相当する記述がなされていました。お詫びして訂正します。


訂正

(1): 「狭義の例外安全」→「広義の例外安全」

例外安全と保証の強度に関する図の修正(強弱関係が逆転してしまっていた)

https://twitter.com/_tusui/status/539725960652668928

(2): 「無矛盾なく」→「矛盾なく」

https://twitter.com/omochimetaru/status/539801870634475520

(3): 『STLport: Exception Handling』に対するURLの誤り(リンク切れと誤って記載)

『STLport: Exception Handling』に対するコメントの修正と注釈*2の追加

https://twitter.com/k_satoda/status/539813017446137856

(4): 「例外中立」、「静的例外安全」、「静的例外中立」の文章を訂正。

(5): 「構成的」→「静的」

typoではなかったものの、不必要な説明だと思われるため訂正

http://qiita.com/Kokudori/items/987073d59529b6c9a37c/patches/7073


参考文献

[1]: 『More Exceptional C++』 Hurb Sutter, ピアソンエデュケーション 2008

[2]: 『Exceptional C++ Style』 Hurb Sutter, ピアソンエデュケーション 2006

[3]: 『Exceptional C++』 Hurb Sutter, ピアソンエデュケーション 2000

[4]: 『STLport: Exception Handling』 http://www.stlport.org/doc/exception_safety.html