あなたの質問は、「例外安全なコードを書くことは非常に難しい」という主張をします。私はまずあなたの質問に答え、それから彼らの背後に隠された質問に答えます。
質問に答える
あなたは本当に例外安全なコードを書いていますか?
もちろんするよ。
これが、 JavaがC ++プログラマとしての魅力の多くを失った理由です(RAIIセマンティクスの欠如)が、余談ですが、これはC ++の質問です。
実際には、STLまたはBoostコードを操作する必要がある場合に必要です。たとえば、C ++スレッド(boost::thread
またはstd::thread
)は例外をスローして正常に終了します。
あなたの最後の「プロダクションレディ」コードは例外的に安全ですか?
あなたはそれがそうであると確信することさえできますか?
例外安全なコードを書くことは、バグのないコードを書くことに似ています。
コードが例外的に安全であることを100%保証することはできません。しかし、その後、よく知られているパターンを使用し、よく知られているアンチパターンを避けて、そのために努力します。
機能する代替案を知っているか、実際に使用していますか?
C ++には実行可能な代替手段はありません(つまり、Cに戻し、C ++ライブラリを回避する必要があります。また、Windows SEHのような外部の驚きもありません)。
例外安全なコードを書く
例外安全なコードを書くには、まず、各命令がどのレベルの例外安全かを知る必要があります。
たとえば、a new
は例外をスローできますが、組み込み(たとえば、intやポインタ)の割り当ては失敗しません。スワップが失敗することはありません(スロースワップを記述しないでください)std::list::push_back
。
例外保証
最初に理解する必要があるのは、すべての関数によって提供される例外保証を評価できる必要があるということです。
- none:コードはそれを提供するべきではありません。このコードはすべてをリークし、最初にスローされた例外でブレークダウンします。
- 基本:これは、最低限提供する必要がある保証です。つまり、例外がスローされても、リソースはリークされず、すべてのオブジェクトがまだ完全です。
- strong:処理は成功するか、例外をスローしますが、スローすると、データは処理がまったく開始されなかった場合と同じ状態になります(これにより、C ++にトランザクション機能が提供されます)。
- nothrow / nofail:処理は成功します。
コードの例
次のコードは正しいC ++のように見えますが、実際には「なし」の保証を提供しているため、正しくありません。
void doSomething(T & t)
{
if(std::numeric_limits<int>::max() > t.integer) // 1. nothrow/nofail
t.integer += 1 ; // 1'. nothrow/nofail
X * x = new X() ; // 2. basic : can throw with new and X constructor
t.list.push_back(x) ; // 3. strong : can throw
x->doSomethingThatCanThrow() ; // 4. basic : can throw
}
この種の分析を念頭に置いて、すべてのコードを記述します。
提供される最低の保証は基本ですが、各命令の順序付けにより、関数全体が「なし」になります。これは、3。スローするとxがリークするためです。
最初に行うことは、関数を「基本」にすることです。つまり、リストが安全に所有するまでxをスマートポインタに入れます。
void doSomething(T & t)
{
if(std::numeric_limits<int>::max() > t.integer) // 1. nothrow/nofail
t.integer += 1 ; // 1'. nothrow/nofail
std::auto_ptr<X> x(new X()) ; // 2. basic : can throw with new and X constructor
X * px = x.get() ; // 2'. nothrow/nofail
t.list.push_back(px) ; // 3. strong : can throw
x.release() ; // 3'. nothrow/nofail
px->doSomethingThatCanThrow() ; // 4. basic : can throw
}
現在、私たちのコードは「基本的な」保証を提供しています。何もリークせず、すべてのオブジェクトが正しい状態になります。しかし、より強力な保証を提供することができます。これはコストがかかる可能性がある場所であり、これがすべての C ++コードが強力なわけではない理由です。試してみよう:
void doSomething(T & t)
{
// we create "x"
std::auto_ptr<X> x(new X()) ; // 1. basic : can throw with new and X constructor
X * px = x.get() ; // 2. nothrow/nofail
px->doSomethingThatCanThrow() ; // 3. basic : can throw
// we copy the original container to avoid changing it
T t2(t) ; // 4. strong : can throw with T copy-constructor
// we put "x" in the copied container
t2.list.push_back(px) ; // 5. strong : can throw
x.release() ; // 6. nothrow/nofail
if(std::numeric_limits<int>::max() > t2.integer) // 7. nothrow/nofail
t2.integer += 1 ; // 7'. nothrow/nofail
// we swap both containers
t.swap(t2) ; // 8. nothrow/nofail
}
操作を並べ替え、最初に作成X
して適切な値に設定しました。いずれかの操作が失敗してt
も変更されないため、操作1〜3は「強力」と見なすことができます。何かがスローt
されても、変更されX
ず、スマートポインターが所有しているためリークしません。
その後、我々はコピー作成t2
のt
何かがスローした場合、7に操作4からこのコピーに、そして仕事をt2
修正されたが、その後、t
まだ元のです。私達はまだ強い保証を提供します。
その後、我々はスワップt
とt2
。スワップ操作はC ++ではnothrowでなければならないので、作成したスワップがnothrowでないことを期待しましょうT
(そうでない場合は、nothrowに書き換えてください)。
したがって、関数の最後に到達すると、すべてが成功し(戻り値の型は必要ありません)、t
その例外的な値になります。失敗した場合t
でも、元の値のままです。
現在、強力な保証を提供することは非常にコストがかかる可能性があるため、すべてのコードに強力な保証を提供するように努めないでください。 、 じゃやれ。関数ユーザーはそれを感謝します。
結論
例外セーフなコードを書くにはある程度の習慣が必要です。使用する各命令によって提供される保証を評価する必要があり、次に、命令のリストによって提供される保証を評価する必要があります。
もちろん、C ++コンパイラは保証をバックアップしません(私のコードでは、保証は@warning doxygenタグとして提供しています)。
通常の障害とバグ
プログラマは、ノーフェイル機能が常に成功することをどのように保証できますか?結局のところ、関数にはバグがある可能性があります。
これは本当です。例外の保証は、バグのないコードによって提供されることになっています。しかし、どの言語でも、関数を呼び出すと、その関数にバグがないことが前提になります。バグを含む可能性から自分自身を守る正気なコードはありません。できる限りコードを記述し、バグがないことを前提に保証を提供します。バグがある場合は修正してください。
例外は例外的な処理の失敗であり、コードのバグではありません。
最後の言葉
さて、問題は「これはそれだけの価値があるのか?」です。
もちろん。"nothrow / no-fail"関数を使用して、関数が失敗しないことを知っていることは大きな恩恵です。同じことが「強力な」関数についても言えます。これにより、コミット/ロールバック機能を備えたデータベースのようなトランザクションセマンティクスでコードを記述できます。コミットはコードの通常の実行であり、例外はロールバックです。
次に、「基本」は、提供すべき最低限の保証です。C ++は非常に強力な言語であり、そのスコープにより、リソースリークを回避できます(ガベージコレクターがデータベース、接続、またはファイルハンドルに提供するのが難しいと思われるもの)。
だから、限り私はそれを見るように、それはあるそれだけの価値。
2010-01-29を編集:スローしないスワップについて
nobarは、「例外安全なコードをどのように作成するのか」の一部であるため、かなり関連性があると私が思うコメントをしました。
- [me]スワップが失敗することは決してありません(スロースワップを作成することもありません)
- [nobar]これは、カスタム記述
swap()
関数に適した推奨事項です。ただし、std::swap()
内部で使用する操作によっては失敗する可能性があることに注意してください。
デフォルトでstd::swap
は、一部のオブジェクトではスローできるコピーと割り当てが作成されます。したがって、デフォルトのスワップがスローされる可能性があり、クラスまたはSTLクラスに対しても使用されます。限り++標準に関してはCとして、スワップ操作のためにはvector
、deque
、そしてlist
、それは可能性があるため、一方、スローされませんmap
比較ファンクタが(参照がコピー建設に投げることができるかどうザ・C ++言語、スペシャルエディション、付録E、E.4.3プログラミング.Swap)。
ベクターのスワップのVisual C ++ 2008実装を見ると、2つのベクターのアロケーターが同じ(つまり、通常の場合)の場合、ベクターのスワップはスローされませんが、アロケーターが異なる場合はコピーが作成されます。したがって、この最後のケースでスローされる可能性があると思います。
したがって、元のテキストは引き続き有効です。スローするスワップを記述しないでください。ただし、nobarのコメントは覚えておく必要があります。スワップするオブジェクトにスローしないスワップがあることを確認してください。
2011-11-06を編集:興味深い記事
Dave Abrahamsは、基本/強力/スロー禁止の保証を提供してくれた記事で、STL例外の安全化に関する彼の経験について述べています。
http://www.boost.org/community/exception_safety.html
7番目の点(例外の安全性のための自動テスト)を見てください。そこでは、すべてのケースがテストされることを確認するために自動ユニットテストに依存しています。この部分は質問作者の「あなたはそれが確かだと確信できますか?」に対する優れた答えだと思います。
2013-05-31を編集:dionadarからのコメント
t.integer += 1;
オーバーフローが発生しないという保証はなく、例外的な安全性はなく、実際には技術的にUBを呼び出す可能性があります。(符号付きオーバーフローはUB:C ++ 11 5/4です。「式の評価中に結果が数学的に定義されていないか、その型の表現可能な値の範囲にない場合、動作は未定義です。」)符号なし整数はオーバーフローしませんが、2 ^#ビットを法とする同値類でそれらの計算を行います。
Dionadarは次の行を参照していますが、実際には未定義の動作があります。
t.integer += 1 ; // 1. nothrow/nofail
ここでの解決策はstd::numeric_limits<T>::max()
、加算を行う前に、整数がすでに最大値になっているかどうかを(を使用して)確認することです。
私のエラーは「通常の障害とバグ」セクション、つまりバグに行きます。それは推論を無効にしません、そしてそれは達成することが不可能であるので例外安全なコードが役に立たないことを意味しません。コンピュータの電源オフ、コンパイラのバグ、さらにはバグやその他のエラーから自分を守ることはできません。完璧を達成することはできませんが、できるだけ近づくことを試みることができます。
Dionadarのコメントを念頭に置いてコードを修正しました。