web-dev-qa-db-ja.com

(本当に)例外安全なコードを書いていますか?

例外処理(EH)が現在の標準であるようであり、Webを検索しても、それを改善または置換しようとする新しいアイデアや方法は見つかりません(いくつかのバリエーションはありますが、新しいものはありません)。

ほとんどの人はそれを無視するか、単に受け入れるように見えますが、EH hasいくつかの大きな欠点:例外はコードには見えず、多くの多くの可能な出口点を作成します。ソフトウェアに関するJoelは それに関する記事 を書きました。 gotoとの比較は完璧で、EHについてもう一度考えさせられました。

EHを避けて、戻り値、コールバック、または目的に合ったものを使用するようにします。しかし、信頼できるコードを記述する必要がある場合、最近ではEHを無視することはできません:それはnewで始まり、0を返すだけでなく、例外をスローする場合があります。これにより、C++コードの任意の行脆弱性が例外になります。そして、C++の基本コードの多くの場所が例外をスローします... std libがそれを行います。

これは不安定な地面を歩く。のように感じます。だから、私たちは今、例外に注意を払わざるを得ません!

しかし、それは難しい、本当に難しい。例外に対して安全なコードを書くことを学ばなければなりません。そして、たとえそれを使った経験があったとしても、安全のためにコードの単一行を再確認する必要があります!または、try/catchブロックをどこにでも配置し始めると、コードが読みにくくなるまで混乱します。

EHは古いクリーンな決定論的アプローチ(戻り値..)を置き換えました。これは、コードに多くの可能性のある出口点を作成し、例外をキャッチするコードの記述を開始する場合(ある時点で実行する必要があります)、コードを介して多数のパスを作成します(catchブロック内のコード、std :: cerr以外のロギング機能が必要なサーバープログラムについて考えます..)。 EHには利点がありますが、それはポイントではありません。

私の実際の質問:

  • あなたは本当に例外安全なコードを書いていますか?
  • 最後の「生産準備完了」コードは例外に対して安全ですか?
  • あなたはそれが確かであるとさえ確信できますか?
  • 機能する代替手段を知っているか、実際に使用していますか?
304
Frunsi

あなたの質問は、「例外に対して安全なコードを書くのは非常に難しい」という主張をします。最初にあなたの質問に答え、次にそれらの背後にある隠れた質問に答えます。

質問に答える

あなたは本当に例外安全なコードを書いていますか?

もちろんするよ。

これは その reason Javaは、C++プログラマー(RAIIセマンティクスの欠如)としての魅力を多く失いましたが、私は余談です。これはC++の質問です。

実際、STLまたはBoostコードを使用する必要がある場合に必要です。たとえば、C++スレッド(boost::threadまたはstd::thread)は例外をスローして正常に終了します。

最後の「生産準備完了」コードは例外に対して安全ですか?

あなたはそれが確かであるとさえ確信できますか?

例外に対して安全なコードを書くことは、バグのないコードを書くことに似ています。

コードが例外的に安全であることを100%確信することはできません。しかし、その後、よく知られているパターンを使用して、よく知られているアンチパターンを避けて、それを目指して努力します。

機能する代替手段を知っているか、実際に使用していますか?

C++にはno実行可能な代替手段があります(つまり、Cに戻ってC++ライブラリを回避する必要があるだけでなく、Windows SEHのような外部の驚きもあります)。

例外安全なコードを書く

例外安全なコードを書くには、first書く各命令がどのレベルの例外安全であるかを知っている必要があります。

たとえば、newは例外をスローできますが、ビルトイン(int、ポインターなど)の割り当ては失敗しません。スワップは失敗しません(スロースワップを記述しないでください)、std::list::Push_backはスローできます...

例外保証

最初に理解することは、すべての関数によって提供される例外保証を評価できる必要があることです。

  1. none:コードはそれを決して提供すべきではありません。このコードはすべてをリークし、最初にスローされた例外でブレークダウンします。
  2. basic:これは最低でも提供する必要がある保証です。つまり、例外がスローされた場合、リソースはリークされず、すべてのオブジェクトはまだ全体
  3. strong:処理は成功するか、例外をスローしますが、スローした場合、データは処理があった場合と同じ状態になりますまったく開始されていません(これにより、C++にトランザクションの力が与えられます)
  4. 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
}

現在、私たちのコードは「基本的な」保証を提供しています。何もリークせず、すべてのオブジェクトが正しい状態になります。しかし、より多く、つまり強力な保証を提供できます。これがcanのコストが高くなる場所であり、これがすべてではない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はリークしません.

次に、tのコピーt2を作成し、操作4から7でこのコピーを処理します。何かがスローされた場合、t2が変更されますが、tは元のままです。引き続き強力な保証を提供します。

次に、tt2を交換します。スワップ操作はC++ではnothrowである必要があるため、Tに書き込んだスワップがnothrowであると期待しましょう(そうでない場合は、throwにならないように書き直してください)。

したがって、関数の最後に到達すると、すべてが成功し(戻り値の型は不要)、tには例外値があります。失敗した場合、tは元の値のままです。

現在、強力な保証を提供するのは非常にコストがかかる可能性があるため、すべてのコードに強力な保証を提供するよう努めてはいけませんが、費用なしでそれを行うことができれば、 じゃやれ。関数ユーザーはあなたに感謝します。

結論

例外に対して安全なコードを書くには多少の習慣が必要です。使用する各命令によって提供される保証を評価する必要があります。次に、命令のリストによって提供される保証を評価する必要があります。

もちろん、C++コンパイラは保証をバックアップしません(私のコードでは、@ warning doxygenタグとして保証を提供します)。これはちょっと悲しいですが、例外セーフコードを書くことを止めることはできません。

通常の障害とバグ

プログラマーは、失敗しない機能が常に成功することをどのように保証できますか?結局のところ、関数にバグがある可能性があります。

これは本当です。例外保証は、バグのないコードによって提供されることになっています。しかし、その後、どの言語でも、関数を呼び出すと、その関数にバグがないことが前提となります。健全なコードは、バグを含む可能性からそれ自体を保護しません。できる限りコードを記述し、バグがないと仮定して保証を提供します。バグがある場合は修正してください。

例外は例外的な処理の失敗であり、コードのバグではありません。

最後の言葉

さて、質問は「これはそれだけの価値があるのか​​?」です。

もちろん。関数が失敗しないことを知っている「nothrow/no-fail」関数を持つことは大きな恩恵です。同じことは、データベースのようなトランザクションセマンティクスで、コミット/ロールバック機能を備えたコードを記述できる「強力な」機能についても言えます。コミットはコードの通常の実行であり、例外はロールバックです。

そして、「基本」はあなたが提供すべき最低限の保証です。 C++は非常に強力な言語であり、そのスコープにより、リソースリーク(ガベージコレクターがデータベース、接続、またはファイルハンドルに提供するのが難しいと感じるもの)を回避できます。

だから、私が見る限り、それは価値がありますis

編集2010-01-29:スローしないスワップについて

nobarは、「例外セーフコードをどのように書くのか」の一部であるため、非常に関連性があると思うコメントを行いました。

  • [me]スワップは失敗しません(スロースワップを記述しないでください)
  • [nobar]これは、カスタム作成のswap()関数の推奨事項です。ただし、std::swap()は、内部で使用する操作に基づいて失敗する可能性があることに注意してください

デフォルトのstd::swapはコピーと割り当てを行いますが、一部のオブジェクトについてはスローできます。したがって、デフォルトのスワップは、クラスまたはSTLクラスに使用される可能性があります。 C++標準に関する限り、vectordeque、およびlistのスワップ操作はスローされませんが、比較ファンクターがコピーの構築時にスローできる場合はmapの場合はスローされます(参照 C++プログラミング言語、特別版、付録E、E.4.3。スワップ)。

ベクターのスワップのVisual C++ 2008実装を見ると、2つのベクターのアロケーターが同じ場合(つまり、通常の場合)、ベクターのスワップはスローされませんが、アロケーターが異なる場合はコピーが作成されます。したがって、この最後のケースでスローされる可能性があると思います。

したがって、元のテキストは引き続き保持されます。スロースワップを記述しないでください。ただし、nobarのコメントは覚えておく必要があります。スワップしているオブジェクトには、スローしないスワップを指定してください。

2011-11-06編集:興味深い記事

Dave Abrahams 、私たちに basic/strong/nothrowの保証 を与えてくれた、STL例外を安全にすることに関する彼の経験の記事で説明されている:

http://www.boost.org/community/exception_safety.html

7番目のポイント(例外安全性の自動テスト)を見てください。ここでは、すべてのケースがテストされていることを確認するために自動化された単体テストに依存しています。この部分は、質問の著者の「あなたはそれが確かであるとさえ確信できますか?「。

編集2013-05-31: dionadar からのコメント

t.integer += 1;は、オーバーフローが発生しないという保証はなく、例外に対して安全ではなく、実際には技術的にUBを呼び出す可能性があります! (符号付きオーバーフローはUB:C++ 11 5/4「式の評価中に、結果が数学的に定義されていないか、その型の表現可能な値の範囲にない場合、動作は未定義です。」)整数はオーバーフローしませんが、2 ^#bitsを法とする等価クラスで計算を行います。

ディオナダルは次の行を参照していますが、実際には未定義の動作があります。

   t.integer += 1 ;                 // 1. nothrow/nofail

ここでの解決策は、追加を行う前に、整数が既にその最大値にあるかどうかを確認することです(std::numeric_limits<T>::max()を使用)。

私のエラーは、「通常の障害とバグ」セクション、つまりバグに含まれます。それは推論を無効にせず、達成することが不可能であるため例外安全コードが役に立たないことを意味しません。コンピューターの電源オフ、コンパイラーのバグ、バグ、その他のエラーから自分を守ることはできません。あなたは完璧を達成することはできませんが、できるだけ近くに取得しようとすることができます。

Dionadarのコメントを念頭に置いてコードを修正しました。

507
paercebal

C++で例外セーフコードを記述することは、多くのtry {} catch {}ブロックを使用することではありません。コードが提供する保証の種類を文書化することです。

Herb Sutterの Guru Of The Week シリーズ、特に分割払い59、60、61を読むことをお勧めします。

要約すると、提供できる例外安全性には3つのレベルがあります。

  • 基本:コードが例外をスローしても、コードはリソースをリークせず、オブジェクトは破壊可能なままです。
  • 強力:コードが例外をスローすると、アプリケーションの状態は変更されません。
  • スローなし:コードは例外をスローしません。

個人的に、私はこれらの記事をかなり遅れて発見したので、私のC++コードの多くは間違いなく例外安全ではありません。

31
Joh

私たちの中には、20年以上にわたって例外を使用している人もいます。たとえば、PL/Iにはそれらがあります。それらが新しく危険な技術であるという前提は、私​​には疑わしいようです。

18
bmargulies

まず第一に(Neilが述べたように)、SEHはMicrosoftの構造化例外処理です。 C++の例外処理と似ていますが、同一ではありません。実際、Visual Studioで必要な場合は C++例外処理を有効にする にする必要があります。デフォルトの動作では、ローカルオブジェクトがすべての場合に破棄されることは保証されません。どちらの場合でも、例外処理は実際にはharderではなく、単なるdifferentです。

さて、実際の質問に。

あなたは本当に例外安全なコードを書いていますか?

はい。私はすべての場合において例外安全なコードを目指しています。リソースへのスコープ付きアクセスにRAIIテクニックを使用して伝道します(例: boost::shared_ptr メモリ用、 boost::lock_guard ロック用)。一般に、 RAII および scope guarding テクニックを一貫して使用すると、例外に対して安全なコードを簡単に記述できます。秘trickは、何が存在し、それをどのように適用するかを学ぶことです。

最後の「生産準備完了」コードは例外に対して安全ですか?

いいえ。そのまま安全です。私は、24時間年中無休の数年間の例外によるプロセス障害は見ていません。完全なコードではなく、適切に記述されたコードを期待しています。例外の安全性を提供することに加えて、上記の手法は、try/catchブロックで達成することがほぼ不可能な方法で正確性を保証します。トップコントロールスコープ(スレッド、プロセスなど)のすべてをキャッチしている場合は、例外に直面して実行し続けることが確実になります( ほとんどの場合 )。同じテクニックを使用すると、例外がどこでもtry/catchブロックがどこでもでなくても正しくを実行し続けることができます。

それが確かだとさえ言えますか?

はい。徹底的なコード監査で確認できますが、実際には誰もそれを行いませんか?定期的なコードレビューと慎重な開発者は、そこに到達するまでに長い道のりを歩んでいます。

機能する代替手段を知っているか、実際に使用していますか?

上位ビットのエンコード状態(ala HRESULTs )やその恐ろしい setjmp() ... longjmp() ハックなど、長年にわたっていくつかのバリエーションを試してきました。これらは両方とも、実際には完全に異なる方法で故障します。


最後に、いくつかのテクニックを適用し、例外に対応して実際に何かを行うことができる場所を慎重に考える習慣を身に付けると、例外に対して安全な非常に読みやすいコードになります。次のルールに従ってこれを要約できます。

  • 特定の例外について何かできる場合にのみtry/catchを表示したい
  • コードで生のnewdeleteを見たいとは思わないでしょう
  • std::sprintfsnprintf、および一般的な配列を回避する-フォーマットにstd::ostringstreamを使用し、配列をstd::vectorおよびstd::stringに置き換えます
  • 疑わしい場合は、独自に展開する前にBoostまたはSTLの機能を探してください

C++で書くつもりなら、例外の適切な使い方を学び、結果コードを忘れることをお勧めします。例外を回避したい場合は、 それらを持たない または それらを安全にする のいずれかである別の言語で記述することを検討してください。 C++を完全に活用する方法を本当に学びたい場合は、 Herb SutterNicolai Josuttis 、および Scott Meyers からいくつかの本を読んでください。

16
D.Shawley

「どの行でもスローできる」という前提の下で、例外に対して安全なコードを書くことはできません。例外安全コードの設計は、コードで期待、観察、フォロー、および実装することになっている特定の契約/保証に大きく依存しています。 neverがスローされることが保証されているコードが絶対に必要です。そこには他の種類の例外保証があります。

言い換えれば、例外安全なコードを作成することは、大体において、プログラムの問題designだけではなく、単なるcodingの問題でもあります。

9
AnT
  • あなたは本当に例外安全なコードを書いていますか?

まあ、私は確かにするつもりです。

  • 最後の「生産準備完了」コードは例外に対して安全ですか?

例外を使用して構築された24時間年中無休のサーバーは24時間年中無休で実行され、メモリリークが発生しないと確信しています。

  • あなたはそれが確かであるとさえ確信できますか?

コードが正しいことを確認するのは非常に困難です。通常、結果だけで行くことができます

  • 機能する代替手段を知っているか、実際に使用していますか?

いいえ。例外を使用することは、過去30年間にプログラミングで使用した他のどの方法よりも簡単で簡単です。

7
anon

SEH例外とC++例外との混乱を別にして、例外はいつでもスローされる可能性があることに注意し、それを念頭に置いてコードを記述する必要があります。例外安全性の必要性は、主にRAII、スマートポインター、およびその他の最新のC++技術の使用を促進するものです。

十分に確立されたパターンに従えば、例外に対して安全なコードを書くことは特に難しくはありません。実際、エラーを処理するコードを書くことはすべての場合に適切です。

5
Mark Bessey

一般的にEHは良いです。しかし、C++の実装は、例外をキャッチするカバレッジがどれほど優れているかを伝えるのが非常に難しいため、あまりフレンドリーではありません。 Javaはこれを簡単にします。考えられる例外を処理しないと、コンパイラは失敗する傾向があります。

4
Mr. Boy

はい、例外的に安全なコードを書くために私は最善を尽くします。

つまり、which行がスローできることに注意してください。誰もができるわけではありません。それを念頭に置くことが非常に重要です。重要なのは、実際に標準で定義されている例外保証について考え、それを満たすコードを設計することです。

強力な例外保証を提供するために、この操作を作成できますか?基本的なものに落ち着く必要がありますか?どの行が例外をスローする可能性があり、それらが発生した場合にオブジェクトを破損しないようにするにはどうすればよいですか?

2
jalf

EclipseとJavaを使って作業するのが本当に好きです(Javaが初めて)。これは、EHハンドラーがない場合にエディターでエラーがスローされるためです。これにより、例外の処理を忘れることが非常に難しくなります...

さらに、IDEツールを使用すると、try/catchブロックまたは別のcatchブロックが自動的に追加されます。

2
Crowe T. Robot

私たちの中には、C++やC#のように不可視にするのではなく、メソッドによってスローされたすべての例外を強制的に宣言するJavaなどの言語を好む人もいます。

適切に行われた場合、例外はエラーリターンコードよりも優れています。他の理由がない限り、手動で呼び出しチェーンに障害を伝播する必要はありません。

そうは言っても、低レベルAPIライブラリプログラミングでは、おそらく例外処理を避け、エラーの戻りコードに固執する必要があります。

私の経験では、C++でクリーンな例外処理コードを書くことは困難です。私はnew(nothrow)をたくさん使うことになります。

2
David R Tribble
  • あなたは本当に例外安全なコードを書いていますか? [そのようなことはありません。管理された環境がない限り、例外はエラーに対するペーパーシールドです。これは最初の3つの質問に適用されます。]

  • 機能する代替手段を知っているか、実際に使用していますか? [何の代替?ここでの問題は、人々が実際のエラーを通常のプログラム操作と区別しないことです。通常のプログラム操作(つまり、ファイルが見つからない)の場合、実際にはエラー処理ではありません。それが実際のエラーである場合、それを「処理」する方法がないか、実際のエラーではありません。ここでの目標は、何が間違っていたかを見つけ、スプレッドシートを停止してエラーをログに記録するか、ドライバーをトースターに再起動するか、ジェット戦闘機がソフトウェアにバグがあり最高の結果が得られる場合でも飛行を続けられるように祈ることです。

2

多くの人(私はほとんどと言ってもいい)がします。

例外について本当に重要なことは、処理コードをまったく記述しない場合、結果は完全に安全で適切に動作することです。パニックに熱心すぎますが、安全です。

actively安全でない何かを得るためにハンドラーでミスをする必要があり、catch(...){}のみがエラーコードを無視することと比較されます。

0
ima