51

C ++でRAIIを使用すればするほど、重要な割り当て解除を行うデストラクタを使用するようになります。現在、割り当て解除(ファイナライズ、ただし、それを呼び出したい場合)は失敗する可能性があります。その場合、例外は、実際には、2階の誰かに割り当て解除の問題を知らせる唯一の方法です。ただし、スタックの巻き戻し中に例外がスローされる可能性があるため、スローデストラクタはお勧めできません。std::uncaught_exception()それがいつ発生するかを知らせますが、それ以上ではありません。したがって、終了前にメッセージをログに記録できるようにする以外に、プログラムを未定義の状態のままにしておく場合を除いて、できることはほとんどありません。とそうでないものもあります。

1つのアプローチは、スローしないデストラクタを使用することです。しかし、多くの場合、それは実際のエラーを隠すだけです。たとえば、デストラクタは、いくつかの例外がスローされた結果として、一部のRAII管理のDB接続を閉じている可能性があり、それらのDB接続は閉じられない可能性があります。これは、プログラムがこの時点で終了しても問題がないことを必ずしも意味するものではありません。一方、これらのエラーをログに記録して追跡することは、実際にはすべての場合の解決策ではありません。そうでなければ、そもそも例外は必要ありませんでした。スローしないデストラクタを使用すると、破棄前に呼び出されるはずの「reset()」関数を作成する必要がありますが、これはRAIIの目的全体を損なうだけです。

もう1つのアプローチは、プログラムを終了させることです。これは、実行できる最も予測可能なことです。

一度に複数のエラーを処理できるように、例外を連鎖させることを提案する人もいます。しかし、正直なところ、C ++で行われることを実際に見たことがなく、そのようなことを実装する方法がわかりません。

つまり、RAIIまたは例外のいずれかです。ではない?私はスローしないデストラクタに傾いています。主にそれが物事を単純に保つからです(r)。しかし、私が言ったように、RAIIを使用すればするほど、重要なことを行うdtorを使用するようになるので、より良い解決策があることを本当に望んでいます。

付録

私が見つけた興味深い話題の記事やディスカッションへのリンクを追加しています:

4

8 に答える 8

18

デストラクタから例外をスローするべきではありません。

注:標準の変更を反映するように更新されました。

C ++ 03
の場合例外がすでに伝播している場合、アプリケーションは終了します。

C ++ 11
の場合デストラクタがnoexcept(デフォルト)の場合、アプリケーションは終了します。

以下はC++11に基づいています

例外がnoexcept関数をエスケープする場合、スタックが巻き戻されても実装によって定義されます。

以下はC++03に基づいています

終了とは、すぐに停止することを意味します。スタックの巻き戻しが停止します。これ以上デストラクタは呼び出されません。すべての悪いもの。こちらの説明を参照してください。

デストラクタから例外をスローする

私は、これがデストラクタをより複雑にするというあなたの論理に(同意しないように)従いません。
スマートポインタを正しく使用すると、すべてが自動化されるため、実際にはデストラクタが簡単になります。各クラスは、パズルの独自の小さなピースを片付けます。ここには脳外科手術やロケット科学はありません。RAIIのもう1つの大きな勝利。

std :: uncaught_exception()の可能性については、なぜそれが機能しないのかについてのHerbSuttersの記事を紹介します

于 2008-10-01T19:19:26.033 に答える
9

元の質問から:

さて、割り当て解除 (ファイナライズ、と呼びたいところですが) が失敗する可能性があります。

リソースのクリーンアップに失敗すると、次のいずれかが示されます。

  1. プログラマ エラー。この場合は、アプリケーションのシナリオに応じて、失敗をログに記録してから、ユーザーに通知するか、アプリケーションを終了する必要があります。たとえば、すでに解放されている割り当てを解放します。

  2. アロケーターのバグまたは設計上の欠陥。ドキュメントを参照してください。おそらく、プログラマーのエラーを診断するのに役立つエラーが存在する可能性があります。上記の項目 1 を参照してください。

  3. そうでなければ、継続できる回復不可能な悪条件。

たとえば、C++ フリー ストアには失敗しない演算子 delete があります。他の API (Win32 など) はエラー コードを提供しますが、プログラマ エラーまたはハードウェア障害が原因で失敗するだけで、エラーはヒープの破損や二重解放などの状態を示します。

回復不可能な悪条件については、DB 接続を使用します。接続が切断されたために接続を閉じることができなかった場合は、問題ありません。投げるな!接続が切断されると接続が閉じられる (はずである) ため、他に何もする必要はありません。どちらかといえば、トレース メッセージをログに記録して、使用上の問題を診断するのに役立ててください。例:

class DBCon{
public:
  DBCon() { 
    handle = fooOpenDBConnection();
  }
  ~DBCon() {
    int err = fooCloseDBConnection();
    if(err){
      if(err == E_fooConnectionDropped){
        // do nothing.  must have timed out
      } else if(fooIsCriticalError(err)){
        // critical errors aren't recoverable.  log, save 
        //  restart information, and die
        std::clog << "critical DB error: " << err << "\n";
        save_recovery_information();
        std::terminate();
      } else {
        // log, in case we need to gather this info in the future,
        //  but continue normally.
        std::clog << "non-critical DB error: " << err << "\n";
      }
    }
    // done!
  }
};

これらの条件のいずれも、2 種類目の巻き戻しを試みることを正当化するものではありません。プログラムが正常に続行できるか (アンワインドが進行中の場合は、例外のアンワインドを含む)、または今ここで終了します。

編集-追加

閉じることができない DB 接続へのある種のリンクを本当に保持できるようにしたい場合(断続的な条件のために接続を閉じることができず、後で再試行したい場合) は、いつでもクリーンアップを延期できます。 :

vector<DBHandle> to_be_closed_later;  // startup reserves space

DBCon::~DBCon(){
  int err = fooCloseDBConnection();
  if(err){
    ..
    else if( fooIsRetryableError(err) ){
      try{
        to_be_closed.push_back(handle);
      } catch (const bad_alloc&){
        std::clog << "could not close connection, err " << err << "\n"
      }
    }
  }
}

非常にきれいではありませんが、それはあなたのために仕事を成し遂げるかもしれません.

于 2008-10-01T22:09:08.667 に答える
6

あなたは2つのことを見ています:

  1. RAII。これにより、スコープを終了したときにリソースがクリーンアップされることが保証されます。
  2. 操作を完了し、それが成功したかどうかを調べること。

RAII は、操作を完了することを約束します (メモリを解放し、フラッシュを試みたファイルを閉じ、コミットを試みたトランザクションを終了します)。しかし、プログラマーが何もしなくても自動的に行われるため、「試みた」操作が成功したかどうかはプログラマーにわかりません。

例外は、何かが失敗したことを報告する 1 つの方法ですが、おっしゃる通り、C++ 言語の制限があり、デストラクタからそれを行うのには適していません [*]。戻り値は別の方法ですが、デストラクタがそれらを使用できないことはさらに明らかです。

したがって、データがディスクに書き込まれたかどうかを知りたい場合、RAII を使用することはできません。RAIIはまだそれを書き込もうとし、ファイルハンドル(DBトランザクションなど)に関連付けられたリソースを解放するため、「RAIIの目的全体を無効にする」わけではありません。RAII ができることは制限されます。データが書き込まれたかどうかはわかりません。そのためにはclose()、値を返したり、例外をスローしたりできる関数が必要です。

[*] 他の言語にも存在する非常に自然な制限です。RAII デストラクタが例外をスローして「何か問題が発生しました!」と言う必要があると考える場合は、既に例外が発生しているときに何かが発生する必要があります。例外を使用することを私が知っている言語では、一度に 2 つの例外を実行することは許可されていません。言語と構文が許可していないだけです。RAII で目的が達成される場合は、例外自体を再定義して、1 つのスレッドで一度に複数の問題が発生し、2 つの例外が外部に伝播され、2 つのハンドラーが呼び出されるようにする必要があります。 1 つずつ処理します。

finally他の言語では、Java でブロックがスローされた場合など、2 番目の例外が最初の例外を覆い隠すことができます。C++ は、2 番目のものを抑制しなければならないと言っていterminateます。いずれの場合も、上位のスタック レベルには両方の障害が通知されません。少し残念なのは、C++ では、もう 1 つの例外が多すぎるかどうかを確実に判断できないことです (uncaught_exceptionそれはわかりませんが、別のことがわかります)。飛行中の例外ではありません。しかし、その場合にそれができたとしても、もう1つが多すぎる場合には、まだ詰め込みます.

于 2011-07-09T09:43:35.467 に答える
5

同僚に例外/RAII の概念を説明したときの質問を思い出します。

とにかく、私はMartin Yorkの答えRAIIと例外に同意します

例外とデストラクタとの取引は何ですか?

多くの C++ 機能は、スローしないデストラクタに依存しています。

実際、RAII の全体的な概念とコード分岐 (リターン、スローなど) との連携は、割り当て解除が失敗しないという事実に基づいています。同様に、オブジェクトに高い例外保証を提供したい場合、一部の関数 (std::swap など) は失敗しないはずです。

デストラクタを介して例外をスローできないという意味ではありません。言語がこの動作をサポートしようとさえしないというだけです。

認可されたらどうなるの?

せっかくなので想像してみましたが・・・

デストラクタがリソースを解放できなかった場合、どうしますか? あなたのオブジェクトはおそらく半分破壊されています。その情報で「外部」キャッチから何をしますか? 再試行?(はいの場合、デストラクタ内から再試行してみませんか?...)

つまり、とにかく半分破壊されたオブジェクトにアクセスできたとしても、オブジェクトがスタック上にある場合はどうなるでしょうか (これが RAII の基本的な動作です)。スコープ外のオブジェクトにどのようにアクセスできますか?

例外内でリソースを送信しますか?

あなたの唯一の希望は、例外内のリソースの「ハンドル」を送信し、キャッチ内のコードを期待することです...もう一度割り当てを解除してみてください(上記を参照)?

さて、面白いことを想像してみてください:

 void doSomething()
 {
    try
    {
       MyResource A, B, C, D, E ;

       // do something with A, B, C, D and E

       // Now we quit the scope...
       // destruction of E, then D, then C, then B and then A
    }
    catch(const MyResourceException & e)
    {
       // Do something with the exception...
    }
 }

ここで、何らかの理由で D のデストラクタがリソースの割り当て解除に失敗したとします。キャッチによってキャッチされる例外を送信するようにコーディングしました。すべてがうまくいっている: 失敗を好きなように処理することができます (建設的な方法でどのようにするかはまだわかりませんが、今は問題ではありません)。

しかし...

複数の例外内で複数のリソースを送信していますか?

ここで、~D が失敗する可能性がある場合、~C も失敗する可能性があります。~B および ~A と同様に。

この単純な例では、「同時に」失敗した (スコープを終了する) 4 つのデストラクタがあります。必要なのは、1 つの例外をキャッチするのではなく、例外の配列をキャッチすることです (このために生成されたコードがスローしないことを願いましょう)。

    catch(const std::vector<MyResourceException> & e)
    {
       // Do something with the vector of exceptions...
       // Let's hope if was not caused by an out-of-memory problem
    }

話を戻しましょう (私はこの音楽が好きです... ): スローされる例外はそれぞれ異なります (原因が異なるため: C++ では、例外は std::exception から派生する必要がないことに注意してください)。ここで、4 つの例外を同時に処理する必要があります。4 つの例外をタイプ別、およびスローされた順序で処理する catch 句をどのように記述できるでしょうか?

また、同じタイプの複数の例外があり、複数回の割り当て解除の失敗によってスローされた場合はどうなるでしょうか? そして、配列の例外配列のメモリを割り当てるときに、プログラムがメモリ不足になり、えっと... メモリ不足の例外をスローした場合はどうなるでしょうか?

割り当て解除が失敗した理由や別の方法で対応する方法を理解するのに時間を費やすのではなく、この種の問題に時間を費やしたいですか?

明らかに、C++ の設計者は実行可能な解決策を見つけられず、そこで損失を削減しました。

問題はRAII対例外ではありません...

いいえ、問題は、何もできないほど失敗することがあるということです。

RAII は、いくつかの条件が満たされている限り、例外とうまく機能します。その中で:デストラクタはスローしませんあなたが対立として見ているのは、例外RAIIという 2 つの「名前」を組み合わせた 1 つのパターンの単なるコーナー ケースです。

デストラクタで問題が発生した場合は、敗北を受け入れ、回復できるものを回復する必要があります。「DB 接続の割り当てを解除できませんでした。申し訳ありません。少なくとも、このメモリ リークを回避して、このファイルを閉じましょう。」

例外パターンは C++ での主要なエラー処理 (と思われる) ですが、それだけではありません。他のエラー/ログ メカニズムを使用して、C++ 例外が解決策ではない例外的な (しゃれを意図した) ケースを処理する必要があります。

あなたは言語の壁に出会ったばかりなので、私が知っている、または聞いたことのない他の言語の壁は、家を壊すことなく正しく通り抜けました (C# の試みは価値のあるものでしたが、Java のものはまだ冗談であり、私を傷つけます) ... 私は、同じ問題で同じように黙って失敗するスクリプト言語については話しません)。

しかし、最終的には、どれだけ多くのコードを記述しても、ユーザーがコンピューターの電源を切ることによって保護されることはありません

あなたができる最善を尽くして、あなたはすでにそれを書いています。私自身の好みは、投げるファイナライズ メソッド、手動でファイナライズされていないリソースをクリーニングするスローしないデストラクタ、およびデストラクタの失敗について警告するログ/メッセージ ボックス (可能であれば) です。

おそらく、あなたは正しい決闘をしていません。「RAII 対 例外」ではなく、「リソースを解放しようとしている 対 破壊の脅威にさらされても、絶対に解放したくないリソース」とすべきです。

:-)

于 2008-10-16T21:51:22.670 に答える
2

1つお聞きしたいのは、終了などの質問は無視して、通常の破壊または例外的な破壊のいずれかにより、プログラムが DB 接続を閉じることができない場合の適切な応答は何だと思いますか。

あなたは「単なるロギング」を除外しているようで、終了することをためらっているようですが、どうするのが最善だと思いますか?

その質問に対する答えがあれば、どのように進めるかについてより良いアイデアが得られると思います.

私には特に明らかな戦略はありません。他のことは別として、データベース接続を閉じることの意味がよくわかりません。close() がスローされた場合、接続の状態はどうなりますか? 閉じているか、まだ開いているか、不確定ですか? 不確定な場合、プログラムを既知の状態に戻す方法はありますか?

デストラクタの失敗は、オブジェクトの作成を元に戻す方法がなかったことを意味します。プログラムを既知の (安全な) 状態に戻す唯一の方法は、プロセス全体を破棄して最初からやり直すことです。

于 2008-10-05T13:47:20.217 に答える
1

破壊が失敗する理由は何ですか? 実際に破壊する前に、それらを処理することを検討してみませんか?

たとえば、データベース接続を閉じる理由として次のことが考えられます。

  • 取引が進行中です。(std::uncaught_exception() を確認してください - true の場合はロールバック、それ以外の場合はコミット - 実際に接続を閉じる前に、別のポリシーがない限り、これらは最も望ましいアクションです。)
  • 接続がドロップされます。(検出して無視します。サーバーは自動的にロールバックします。)
  • その他の DB エラー。(ログに記録して、調査し、将来的に適切に処理できるようにします。これは、検出して無視する可能性があります。それまでの間、ロールバックを試して再度切断し、すべてのエラーを無視してください。)

私が RAII を正しく理解していれば (そうではないかもしれませんが)、要点はその範囲です。とにかく、オブジェクトよりも長く続くトランザクションを望んでいるわけではありません。ですから、できる限り確実に閉鎖したいと考えるのは理にかなっているように思えます。RAII はこれを独自のものにしません。オブジェクトがまったくなくても (たとえば C で)、すべてのエラー状態をキャッチして、できる限り最善の方法で処理しようとします (これは、場合によってはそれらを無視することになります)。RAII が行うことは、そのリソース タイプを使用する関数の数に関係なく、すべてのコードを 1 つの場所に配置することを強制することだけです。

于 2008-10-05T14:06:50.957 に答える
0

チェックすることで、現在実行中の例外があるかどうかを確認できます (たとえば、スタックの巻き戻し、おそらく例外オブジェクトのコピーなどを実行している throw ブロックと catch ブロックの間)。

bool std::uncaught_exception()

true が返された場合、この時点でスローするとプログラムが終了します。そうでない場合は、スローしても安全です (または、少なくともこれまでと同じくらい安全です)。これについては、ISO 14882 (C++ 標準) のセクション 15.2 および 15.5.3 で説明されています。

これは、例外のクリーンアップ中にエラーが発生したときに何をすべきかという質問には答えていませんが、実際にはそれに対する良い答えはありません. ただし、単にパニックするのではなく、後者の場合に別のことをするのを待つ場合(ログして無視するなど)、通常の終了と例外的な終了を区別できます。

于 2008-10-01T19:33:01.833 に答える
0

ファイナライズプロセス中に何らかのエラーを処理する必要がある場合は、デストラクタ内で実行しないでください。代わりに、エラー コードを返すかスローする別の関数を使用する必要があります。コードを再利用するには、デストラクタ内でこの関数を呼び出すことができますが、例外が漏れないようにする必要があります。

一部の人が言及したように、これは実際にはリソースの割り当て解除ではなく、終了時のリソース コミットのようなものです。他の人が言ったように、強制電源オフ中に保存が失敗した場合はどうすればよいですか? 万能な答えはおそらくありませんが、次のアプローチのいずれかをお勧めします。

  • 失敗と損失が起こるのをただ許してください
  • 保存されていない部分を別の場所に保存し、後で復元できるようにします (これも機能しない場合は、他の方法を参照してください)。

このアプローチのいずれかが気に入らない場合は、ユーザーに明示的に保存させてください。電源オフ中はオートセーブに頼らないように伝えてください。

于 2021-09-29T11:32:09.300 に答える