RAII を使用して例外をネストする
-
21-12-2019 - |
質問
したがって、C++で例外をネストする方法は次のとおりです。 std::nested_exception
は:
void foo() {
try {
// code that might throw
std::ifstream file("nonexistent.file");
file.exceptions(std::ios_base::failbit);
}
catch(...) {
std::throw_with_nested(std::runtime_error("foo failed"));
}
}
しかし、この手法では、例外をネストしたいすべてのレベルで明示的な try/catch ブロックが使用されており、控えめに言っても醜いものです。
RAII、 ジョン・カルブが拡大 「責任の取得は初期化である」ため、明示的な try/catch ブロックを使用する代わりに、例外を処理するはるかにクリーンな方法です。RAII では、明示的な try/catch ブロックは主に、最終的に例外を処理するためにのみ使用されます。ユーザーにエラーメッセージを表示するため。
上記のコードを見ると、次のように入力すると思われます foo()
あらゆる例外を次のように報告する責任が伴うと見なすことができます。 std::runtime_error("foo failed")
詳細をnested_Exception内にネストします。RAII を使用してこの責任を取得できれば、コードはよりきれいに見えます。
void foo() {
Throw_with_nested on_error("foo failed");
// code that might throw
std::ifstream file("nonexistent.file");
file.exceptions(std::ios_base::failbit);
}
ここで RAII 構文を使用して明示的な try/catch ブロックを置き換える方法はありますか?
これを行うには、デストラクターが呼び出されたときに、そのデストラクター呼び出しが例外によるものであるかどうかを確認し、例外によるものである場合はその例外をネストし、アンワインドが正常に続行できるように新しいネストされた例外をスローする型が必要です。それは次のようになります。
struct Throw_with_nested {
const char *msg;
Throw_with_nested(const char *error_message) : msg(error_message) {}
~Throw_with_nested() {
if (std::uncaught_exception()) {
std::throw_with_nested(std::runtime_error(msg));
}
}
};
しかし std::throw_with_nested()
「現在処理されている例外」がアクティブである必要があります。これは、catch ブロックのコンテキスト内以外では機能しないことを意味します。したがって、次のようなものが必要になります。
~Throw_with_nested() {
if (std::uncaught_exception()) {
try {
rethrow_uncaught_exception();
}
catch(...) {
std::throw_with_nested(std::runtime_error(msg));
}
}
}
残念ながら、私の知る限り、そのようなものはありません rethrow_uncaught_excpetion()
C++で定義されています。
解決
デストラクター内でキャッチされなかった例外をキャッチ (および消費) するメソッドが存在しない場合、ネストされているかどうかにかかわらず、デストラクターのコンテキストで例外を再スローする方法はありません。 std::terminate
呼び出されているとき (例外処理のコンテキストで例外がスローされたとき)。
std::current_exception
(と組み合わせ std::rethrow_exception
) は、現在処理されている例外へのポインタのみを返します。この場合の例外は明示的に処理されないため、このシナリオでは使用できなくなります。
上記を考慮すると、唯一の答えは美的観点からのものです。関数レベルの try ブロックを使用すると、見た目の醜さが少し軽減されます。(好みのスタイルに合わせて調整してください):
void foo() try {
// code that might throw
std::ifstream file("nonexistent.file");
file.exceptions(std::ios_base::failbit);
}
catch(...) {
std::throw_with_nested(std::runtime_error("foo failed"));
}
他のヒント
RAⅡでは無理
簡単なルールで考えると
デストラクターは決してスローしてはなりません。
RAII では、希望するものを実装することは不可能です。このルールには単純な理由が 1 つあります。スタックの巻き戻し中に、処理中の例外が原因でデストラクターが例外をスローした場合、 terminate()
が呼び出されると、アプリケーションは停止します。
代替案
C++11 ではラムダを使用できるため、作業が少し楽になります。あなたは書ける
void foo()
{
giveErrorContextOnFailure( "foo failed", [&]
{
// code that might throw
std::ifstream file("nonexistent.file");
file.exceptions(std::ios_base::failbit);
} );
}
関数を実装すると giveErrorContextOnFailure
次の方法で:
template <typename F>
auto giveErrorContextOnFailure( const char * msg, F && f ) -> decltype(f())
{
try { return f(); }
catch { std::throw_with_nested(std::runtime_error(msg)); }
}
これにはいくつかの利点があります。
- エラーがどのようにネストされるかをカプセル化します。
- この手法をプログラム全体で厳密に実行する場合、エラーのネスト方法の変更はプログラム全体で変更できます。
- エラーメッセージはRAIIと同様にコードの前に記述できます。この手法は、ネストされたスコープにも使用できます。
- コードの繰り返しが少なくなります。書く必要はありません
try
,catch
,std::throw_with_nested
そしてstd::runtime_error
. 。これにより、コードの保守がより容易になります。プログラムの動作を変更したい場合は、コードを 1 か所だけ変更する必要があります。 - 戻り値の型は自動的に推定されます。したがって、あなたの関数の場合、
foo()
何かを返す必要がある場合は、単に追加するだけですreturn
前にgiveErrorContextOnFailure
関数 foo() で。
リリース モードでは、テンプレートがデフォルトでインライン化されているため、通常、try-catch の方法と比較してパフォーマンス パネルはありません。
もう 1 つ従うべき興味深いルールがあります。
使ってはいけません
std::uncaught_exception()
.
素敵なものがあります これに関する記事 このルールを完璧に説明している Herb Sutter のトピック。要するに:機能があれば f()
と呼ばれるもの スタックの巻き戻し中にデストラクター内から このように見えます
void f()
{
RAII r;
bla();
}
ここでデストラクタは RAII
のように見える
RAII::~RAII()
{
if ( std::uncaught_exception() )
{
// ...
}
else
{
// ...
}
}
その場合、スタックの巻き戻し中に外側のデストラクター内で行われるため、デストラクター内の最初の分岐が常に選択されます。 std::uncaught_exception()
のデストラクターを含む、そのデストラクターから呼び出された関数内であっても、常に true を返します。 RAII
.