1. ホーム
  2. c++

[解決済み】あなたは(本当に)例外安全なコードを書いていますか?[クローズド]

2022-03-25 16:12:31

質問

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

ほとんどの人は無視するか、そのまま受け入れているようですが、EHは があります。 例外はコードから見えないし、たくさんの出口を作ることになるからです。ソフトウェアに関するJoelは についての記事 . との比較は goto がぴったりで、EHについて改めて考えさせられました。

私はEHを避け、戻り値やコールバックなど、目的に合ったものを使うようにしています。しかし 信頼性の高いコードを書かなければならないとき、最近のEHは無視できない。 : で始まる。 new を返すだけでなく、例外を投げることもあります(昔と同じ)。このため、C++のコードであれば、どんな行でも 脆弱 例外が発生します。そして、C++の基礎となるコードでは、さらに多くの場所で例外が投げられます...std libがそうです、そして以下もそうです。

これは、次のように感じます。 不安定な土地で歩く .. というわけで、今度は例外処理に注意することを余儀なくされました

でも、難しいんですよ、本当に難しいんです。例外安全コードの書き方を学ばなければならないし、たとえ経験があったとしても、コードの一行一行が安全かどうかダブルチェックする必要があるのです あるいは、try/catchブロックをあちこちに置くようになり、読みにくい状態になるまでコードが乱雑になる。

EH は、古いクリーンな決定論的アプローチ (戻り値...) に取って代わりました。このアプローチには、ほんの少しですが理解でき、簡単に解決できる欠点がありましたが、コード内に多くの可能な終了点を作り出すアプローチになりました。EHには利点がありますが、それは問題ではありません。

私の実際の質問です。

  • 本当に例外安全なコードを書いているのですか?
  • 最後に作成したquot;production ready"のコードが例外安全であることを確認していますか?
  • 本当にそうでしょうか?
  • 効果のある代替手段を知っているか、または実際に使っているか?

解決方法は?

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

質問に対する回答

<ブロッククオート

本当に例外安全なコードを書いているのですか?

もちろん、そうです。

これは その C++プログラマの私にとってJavaが魅力を失った理由(RAIIセマンティクスの欠如)ですが、話がそれましたね。これはC++の問題なのです。

STLやBoostのコードを扱う必要があるときは、実際、必要です。例えば、C++のスレッド( boost::thread または std::thread ) は、例外を投げて優雅に終了します。

最後に書いたquot;production ready"のコードは、本当に例外安全なのでしょうか?

本当にそうでしょうか?

例外安全なコードを書くことは、バグのないコードを書くことと同じです。

自分のコードが例外安全であることを100%保証することはできない。しかし、よく知られたパターンを使い、よく知られたアンチパターンを避けながら、そのために努力するのです。

効果的な代替手段を知っていますか、または実際に使っていますか?

があります。 いいえ C++で実行可能な代替案(つまり、Cに戻してC++のライブラリを避ける必要がありますし、Windows SEHのような外部の驚きもあります)。

例外安全コードの書き方

例外安全なコードを書くためには、以下のことを知る必要があります。 まず を使用し、各命令がどの程度の例外安全性を持つかを記述します。

例えば new は例外を投げることができますが、組み込み(例えばintやポインタ)の代入は失敗しません。スワップは決して失敗しません(スワップを投げるような書き方は絶対にしないでください)。 std::list::push_back を投げることができる...

例外保証

まず理解すべきは、すべての関数が提供する例外保証を評価できるようにすることです。

  1. なし : あなたのコードは決してそれを提供してはいけません。このコードはすべてをリークし、最初の例外が発生した時点で破綻してしまいます。
  2. 基本 : これは最低限提供しなければならない保証で、つまり、例外が発生しても、リソースは漏れず、すべてのオブジェクトはまだ全体である
  3. 強い : 処理は成功するか例外を投げるが、もし例外を投げた場合、データは処理が全く開始されなかったのと同じ状態になる(これはC++にトランザクションの力を与える)
  4. ノットスロー/ノーフェール : 処理は成功します。

コード例

以下のコードは正しいC++のように見えますが、実は"none"保証を提供しているため、正しくありません。

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
}

私は、このような解析を意識して、すべてのコードを書いています。

提供される最低保証は基本的なものですが、その後、各命令の順序によって関数全体が "none"になり、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 はスマートポインタが所有しているため、リークしません。

次に、コピー t2t このコピーに対して、4から7までの操作を行います。 t2 が修正されますが、その後 t がオリジナルであることに変わりはありません。強力な保証を提供することに変わりはありません。

次に tt2 . C++では、スワップ操作は無視されるはずです。 T はnothrowです(nothrowでない場合はnothrowになるように書き換えてください)。

つまり、関数の最後まで到達すれば、すべてが成功し(戻り値の型は必要ない)、かつ t はその例外的な値を持っています。もし失敗したら t は元の値のままです。

強力な保証を提供するにはかなりのコストがかかるので、すべてのコードに強力な保証を提供しようとするのではなく、コストをかけずにできるのであれば(C++のインライン化やその他の最適化によって上記のコードをすべてコストなしにできる)、そうしてください。関数の利用者はそのことに感謝することでしょう。

まとめ

例外安全なコードを書くには、ある程度の習慣が必要です。使用する各命令が提供する保証を評価する必要があり、そして、命令のリストが提供する保証を評価する必要があります。

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

正常な障害とバグ

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

これは本当です。例外の保証は、バグのないコードによって提供されるはずです。しかし、それでは、どんな言語でも、関数を呼び出すということは、その関数にバグがないことを前提にしていることになります。まともなコードであれば、バグがある可能性から自分自身を保護することはない。できる限り最善のコードを書き、そして、バグがないことを前提に保証を提供する。そして、もしバグがあれば、それを修正することです。

例外は例外的な処理の失敗のためにあるのであって、コードのバグのためにあるのではありません。

最後の言葉

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

もちろん、そうです。nothrow/no-fail/quot "関数を持つことは、その関数が失敗しないことを知る上で、大きな恩恵となる。同じことが"strong"関数にも言えます。この関数を使えば、データベースのようなトランザクションのセマンティクスを持つコードを、コミット/ロールバック機能で記述することができます。

それから、"basic"は、最低限提供すべき保証です。C++はスコープを持つ非常に強力な言語であり、リソースリークを回避することができます(ガベージコレクタはデータベース、接続、ファイルハンドルに対して提供することが困難であることを発見するでしょう)。

つまり、私が見る限りでは の価値がある。

2010-01-29を編集。非投げスワップについて

nobar さんのコメントは、「例外安全コードの書き方」の一部であるため、非常に適切だと思います。

  • [私】スワップは絶対に失敗しない(投げるスワップも書くな)
  • [nobar] これは、カスタムで書かれた推奨の swap() 関数を使用することができます。しかし、注意しなければならないのは std::swap() は、内部で使用する操作によって失敗することがあります。

デフォルトの std::swap はコピーと代入を行うので、オブジェクトによってはスローする可能性があります。したがって、デフォルトのスワップは、あなたのクラスや、STLのクラスで使用されても、スローする可能性があります。C++の標準では、スワップは vector , deque および list は投げませんが map 比較ファンクタがコピー構築時に投げる可能性がある場合 ( C++プログラミング言語特別編、付録E、E.4.3.Swap ).

Visual C++ 2008 の実装を見ると、vector の swap は、2つの vector が同じアロケータを持つ場合(つまり通常の場合)にはスローしませんが、異なるアロケータを持つ場合にはコピーを作成します。従って、この最後のケースで投げる可能性があると推測されます。

というわけで、原文はそのままです。しかし、nobar さんのコメントは忘れてはいけません。スワップするオブジェクトが throwing でないスワップであることを確認すること。

編集 2011-11-06: 面白い記事がありました。

デイブ・エイブラハムズ を与えてくれた。 ベーシック/ストロング/ノーズロー保証 は、STLの例外を安全にするための彼の経験を記事で説明しています。

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

7番目のポイント(Automated testing for exception-safety)では、すべてのケースがテストされていることを確認するために、自動ユニットテストに頼っています。この部分は、質問者の"に対する素晴らしい回答だと思います。 本当にそうでしょうか? "。

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() ) を行ってから足し算を行います。

私のミスは、"Normal failure vs. bug"の項、つまりバグに入るでしょう。 また、例外安全なコードを実現することが不可能であるため、役に立たないということでもありません。 コンピュータのスイッチオフやコンパイラのバグ、あるいは自分のバグ、その他のエラーから自分を守ることはできません。完璧を目指すことはできないが、可能な限り近づけようとすることはできる。

Dionadarさんのコメントを参考に、コードを修正しました。