例外処理 (EH) は現在の標準のようですが、Web で検索しても、それを改善または置き換えようとする新しいアイデアや方法は見つかりません (まあ、いくつかのバリエーションは存在しますが、目新しいものはありません)。
ほとんどの人はそれを無視するか、ただ受け入れているようですが、EHには大きな欠点があります。例外はコードからは見えず、非常に多くの終了ポイントを作成します。ソフトウェアに関するJoelは、それに関する記事との比較がgoto
ぴったりで、EHについて改めて考えさせられました。
私は EH を避けて、戻り値、コールバック、または目的に合ったものだけを使用するようにしています。しかし、信頼性の高いコードを記述する必要がある場合、最近では EH を無視することはできません。EH は で始まりnew
、例外をスローする場合があります (昔のように 0 を返すだけ)。これにより、C++ コードのほぼすべての行が例外に対して脆弱になります。そして、C++ の基礎コードのさらに多くの場所で例外がスローされます... std lib でも同様です。
これは不安定な地面の上を歩いているような気がします。そのため、例外について注意を払う必要があります。
しかし、それは難しい、本当に難しいのです。例外安全なコードの書き方を学ばなければなりません。たとえ経験があっても、安全であるかどうかはコードの 1 行ごとに二重チェックする必要があります。あるいは、あらゆるところに try/catch ブロックを配置し始め、コードが乱雑になり、読みにくくなってしまいます。
EH は、少数ではあるものの理解しやすく簡単に解決できる欠点を持つ、古いクリーンな決定論的アプローチ (戻り値など) を、コード内に多数の終了ポイントを作成するアプローチに置き換えました。また、例外をキャッチするコード (ある時点で強制的に実行する必要があるもの) を書き始めると、コード内に多数のパスが作成されます (catch ブロック内のコード、std::cerr 以外のログ機能が必要なサーバー プログラムについて考えてみてください)。EH には利点がありますが、それは重要ではありません。
私の実際の質問:
- 本当に例外安全なコードを書いていますか?
- 最新の「本番環境対応」コードは例外安全ですか?
- 本当にそうだと確信できますか?
- 効果的な代替手段を知っていますか、また実際に使用していますか?
ベストアンサー1
あなたの質問は、「例外安全なコードを書くのは非常に難しい」と主張しています。まずあなたの質問に答え、次にその背後にある隠れた質問に答えたいと思います。
質問に答える
本当に例外安全なコードを書いていますか?
もちろんするよ。
これが、C++ プログラマーである私にとって Java の魅力が大きく失われた理由です (RAII セマンティクスの欠如)。しかし、話がそれてしまいました。これは C++ に関する質問です。
実際、STL または Boost コードで作業する必要がある場合は、これが必要になります。たとえば、C++ スレッド (boost::thread
またはstd::thread
) は、正常に終了するために例外をスローします。
最新の「本番環境対応」コードは例外安全ですか?
本当にそうだと確信できますか?
例外安全なコードを書くことは、バグのないコードを書くことと同じです。
コードが例外安全であることを 100% 確信することはできません。しかし、既知のパターンを使用し、既知のアンチパターンを回避することで、例外安全を目指します。
効果的な代替手段を知っていますか、また実際に使用していますか?
C++ には実行可能な代替手段はありません(つまり、C に戻って C++ ライブラリや、Windows SEH などの外部のサプライズを回避する必要があります)。
例外安全なコードを書く
例外安全なコードを書くには、まず、書く各命令がどのようなレベルの例外安全性を持っているかを知る必要があります。
たとえば、はnew
例外をスローできますが、組み込み (int やポインタなど) の割り当ては失敗しません。 swap は決して失敗しません (例外をスローする swap を記述しないでください)。 は例外を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
、このコピーに対して操作 4 から 7 を実行します。何かがスローされた場合、t2
は変更されますが、 はt
元の状態のままです。引き続き強力な保証を提供します。
次に、 swapt
と をt2
切り替えます。swap 操作は C++ では nothrow であるはずなので、記述した swap が nothrow であることを期待しましょうT
(そうでない場合は、nothrow になるように書き直してください)。
したがって、関数の最後まで到達すると、すべてが成功し (戻り値の型は不要)、t
期待された値を持ちます。失敗した場合は、t
元の値のままです。
さて、強力な保証を提供するにはかなりのコストがかかる可能性があるため、すべてのコードに強力な保証を提供しようとする必要はありませんが、コストをかけずに実行できる場合 (C++ のインライン化やその他の最適化により、上記のすべてのコードがコストなしで実行できる場合) は、そうしてください。関数のユーザーはそれに感謝するでしょう。
結論
例外安全なコードを書くには、ある程度の習慣が必要です。使用する各命令によって提供される保証を評価し、次に命令リストによって提供される保証を評価する必要があります。
もちろん、C++ コンパイラーは保証をバックアップしません (私のコードでは、@warning doxygen タグとして保証を提供しています)。これは少し残念ですが、例外安全なコードを記述しようとするのを止めるべきではありません。
通常の障害とバグ
プログラマーは、失敗しない関数が常に成功することをどのようにして保証できるでしょうか? 結局のところ、関数にはバグがある可能性があります。
これは本当です。例外の保証は、バグのないコードによって提供されるはずです。しかし、どの言語でも、関数を呼び出すということは、その関数にバグがないことを前提としています。まともなコードで、バグの可能性から自分自身を守るものはありません。できる限り最善のコードを書いて、バグがないことを前提として保証を提供してください。そして、バグがある場合は修正してください。
例外は例外的な処理の失敗に対するものであり、コードのバグに対するものではありません。
最後の言葉
さて、問題は「これは価値があるのか?」ということです。
もちろん、その通りです。関数が失敗しないことがわかっている「nothrow/no-fail」関数があることは、大きな利点です。同じことは「strong」関数にも言えます。これにより、コミット/ロールバック機能を備えたデータベースのようなトランザクション セマンティクスを持つコードを記述でき、コミットは通常のコード実行であり、例外をスローするとロールバックになります。
そして、「基本」はあなたが提供すべき最低限の保証です。C++ はスコープを備えた非常に強力な言語であり、リソース リーク (データベース、接続、またはファイル ハンドルに対してガベージ コレクターが提供しにくいもの) を回避できます。
ですから、私から見れば、それは価値があると思います。
2010-01-29 編集: スローしない swap について
nobar は、「例外安全なコードをどのように記述するか」という部分に関するコメントをしましたが、これは非常に関連性が高いと思います。
- [私] スワップは失敗しません(スロースワップを書かないでください)
- [nobar] これはカスタム関数に適した推奨事項です。ただし、内部で使用する操作によっては失敗する可能性がある
swap()
ことに注意してください。std::swap()
デフォルトでは、std::swap
コピーと割り当てが行われ、一部のオブジェクトでは例外をスローする可能性があります。したがって、デフォルトの swap は、独自のクラスまたは STL クラスに使用する場合でも例外をスローする可能性があります。C++ 標準に関する限り、、、およびの swap 操作はvector
例外deque
をlist
スローしませんが、比較関数がコピー構築時に例外をスローできる場合は、例外をスローする可能性があります( 「C++ プログラミング言語、特別版」の付録 E、E.4.3.Swap をmap
参照してください)。
Visual C++ 2008 のベクトルの swap の実装を見ると、2 つのベクトルが同じアロケータを持つ場合 (つまり、通常のケース)、ベクトルの swap は例外をスローしませんが、異なるアロケータを持つ場合はコピーを作成します。したがって、この最後のケースでは例外をスローする可能性があると想定します。
したがって、元のテキストは依然として有効です: スロー スワップを決して記述しないでください。ただし、nobar のコメントは覚えておく必要があります: スワップするオブジェクトには、スローしないスワップがあることを確認してください。
2011-11-06編集: 興味深い記事
デイブ・エイブラハムズ、私たちに与えてくれた基本/強力/nothrow保証は、STL 例外を安全にする方法についての記事で自身の経験を説明しています。
http://www.boost.org/community/exception_safety.html
7 番目のポイント (例外安全性のための自動テスト) を見てください。ここでは、すべてのケースがテストされていることを確認するために、自動ユニット テストに依存しています。この部分は、質問の作成者の「それが確実かどうかは確かですか?」に対する優れた回答だと思います。
編集 2013-05-31: コメントディオナダル
t.integer += 1;
オーバーフローが発生しないという保証はなく、例外安全ではなく、実際技術的に UB を呼び出す可能性があります。(符号付きオーバーフローは UB です: C++11 5/4「式の評価中に結果が数学的に定義されていない場合、またはその型の表現可能な値の範囲内にない場合、動作は未定義です。」) 符号なし整数はオーバーフローしませんが、2^# ビットを法とする同値クラスで計算を行うことに注意してください。
Dionadar は、実際には未定義の動作を持つ次の行を参照しています。
t.integer += 1 ; // 1. nothrow/nofail
std::numeric_limits<T>::max()
ここでの解決策は、加算を行う前に、整数がすでに最大値に達しているかどうかを確認することです ( を使用)。
私のエラーは「通常の失敗とバグ」のセクション、つまりバグに分類されます。これは推論を無効にするものではなく、例外セーフなコードが実現不可能であるために役に立たないことを意味するものでもありません。コンピュータの電源オフ、コンパイラのバグ、さらにはバグやその他のエラーから身を守ることはできません。完璧を達成することはできませんが、可能な限りそれに近づくように努めることはできます。
Dionadar のコメントを参考にしてコードを修正しました。