C++ コア ガイドライン:goto は悪とみなされる

例外をスローできず、final_action を使用できない場合 (finally ) ガイドライン サポート ライブラリから、問題があります。例外的な状態には例外的なアクションが必要です:goto. 本当ですか?

goto exit; に関するガイドラインを読んで正直驚いた 最後の救出として。 C++ コア ガイドラインの残りのエラー処理規則は次のとおりです。

  • E.25:例外をスローできない場合は、リソース管理のために RAII をシミュレートします
  • E.26:例外をスローできない場合は、すぐに失敗することを検討してください
  • E.27:例外をスローできない場合は、体系的にエラー コードを使用する
  • E.30:例外指定を使用しない
  • E.31:catch を適切に並べ替えます -句

最初の 3 つのルールはかなり関連しています。したがって、それらについて一緒に書きます。

E5:例外をスローできない場合は、リソース管理のために RAII をシミュレートします。E.26:例外をスローできない場合は、すぐに失敗することを検討してください。E.27:例外をスローできない場合は、体系的にエラー コードを使用してください

RAII の考え方は非常に単純です。リソースを処理する必要がある場合は、リソースをクラスに入れます. 初期化にはクラスのコンストラクタを使用し、リソースの破棄にはデストラクタを使用します。スタック上にクラスのローカル インスタンスを作成すると、C++ ランタイムがリソースを処理して完了です。 RAII の詳細については、以前の投稿ガベージ コレクション - ノー サンクスをお読みください。

リソース管理のために RAII をシミュレートするとはどういう意味ですか?関数 func があると想像してください Gadget の場合を除いて存在します 作成できません。

void func(zstring arg)
{
 Gadget g {arg};
 // ...
}

例外をスローできない場合は、valid を追加して RAII をシミュレートする必要があります。 Gadget. へのメソッド

error_indicator func(zstring arg)
{
 Gadget g {arg};
 if (!g.valid()) return gadget_construction_error;
 // ...
 return 0; // zero indicates "good"
}

この場合、呼び出し元は戻り値をテストする必要があります。

ルール E.26 は簡単です。メモリ不足などのエラーから回復する方法がない場合は、すぐに失敗します。例外をスローできない場合は、std::abort を呼び出します プログラムの異常終了を引き起こします。

void f(int n)
{
 // ...
 p = static_cast<X*>(malloc(n, X));
 if (!p) abort(); // abort if memory is exhausted
 // ...
}

std::abort シグナル SIGABRT をキャッチするシグナルハンドラをインストールしない場合にのみ、プログラムの異常終了が発生します。

関数 f は、次の関数のように動作します:

void f(int n)
{
 // ...
 p = new X[n]; // throw if memory is exhausted (by default, terminate)
 // ...
}

では、単語以外の goto について書きます。 ルール E.27.

エラーが発生した場合は、ガイドラインに従っていくつかの問題を解決する必要があります:

<オール>
  • 関数の外からエラー インジケータを送信するにはどうすればよいですか?
  • エラー終了する前に、関数からすべてのリソースを解放するにはどうすればよいですか?
  • エラー インジケーターとして何を使用しますか?
  • 通常、関数には 2 つの戻り値が必要です。値とエラー インジケータ。したがって、std::pair ぴったりです。クリーンアップ コードを関数にカプセル化したとしても、リソースの解放は簡単にメンテナンスの悪夢になる可能性があります。

    std::pair<int, error_indicator> user()
    {
     Gadget g1 = make_gadget(17);
     if (!g1.valid()) {
     return {0, g1_error};
     }
    
     Gadget g2 = make_gadget(17);
     if (!g2.valid()) {
     cleanup(g1);
     return {0, g2_error};
     }
    
     // ...
    
     if (all_foobar(g1, g2)) {
     cleanup(g1);
     cleanup(g2);
     return {0, foobar_error};
     // ...
    
     cleanup(g1);
     cleanup(g2);
     return {res, 0};
    }
    

    わかりました、それは正しいようです!それとも?

    DRYとは何か知っていますか を意味する? D Rではありません はいを繰り返す 私たち自身。クリーンアップ コードは関数にカプセル化されていますが、クリーンアップ関数はさまざまな場所で呼び出されるため、コードにはコードの繰り返しの匂いがあります。どうすれば繰り返しを取り除くことができますか?関数の最後にクリーンアップ コードを配置して、そこにジャンプするだけです。

    std::pair<int, error_indicator> user()
    {
     error_indicator err = 0;
    
     Gadget g1 = make_gadget(17);
     if (!g1.valid()) {
     err = g1_error; // (1)
     goto exit;
     }
    
     Gadget g2 = make_gadget(17);
     if (!g2.valid()) {
     err = g2_error; // (1)
     goto exit;
     }
    
     if (all_foobar(g1, g2)) {
     err = foobar_error; // (1)
     goto exit;
     }
     // ...
    
    exit:
     if (g1.valid()) cleanup(g1);
     if (g2.valid()) cleanup(g2);
     return {res, err};
    }
    

    goto の助けを借りて認めた 関数の全体的な構造は非常に明確です。エラーの場合は、エラー インジケータ (1) だけが設定されます。例外的な状態には、例外的なアクションが必要です。

    E.30:例外指定を使用しない

    まず、例外指定の例を次に示します。

    int use(int arg)
     throw(X, Y)
    {
     // ...
     auto x = f(arg);
     // ...
    }
    

    これは、関数の使用により、タイプ X の例外をスローできる可能性があることを意味します 、または Y .別の例外がスローされた場合、std::terminate

    引数 throw(X, Y による動的例外指定 ) 引数なし throw() C++11 以降は非推奨です。引数付きの動的例外指定は C++17 で削除されますが、引数なしの動的例外指定は C++20 で削除されます。 throw() noexcept. と同等です 詳細は次のとおりです。 C++ コア ガイドライン:noexcept 指定子と演算子。

    最後のルールを知らないと、非常に驚​​くかもしれません。

    E.31:catch を正しく注文してください -句

    例外は、最適な戦略に従ってキャッシュされます。これは、実際の例外に適合する最初の例外ハンドラが使用されることを意味します。これが、例外ハンドラーを特定のものから一般的なものに構造化する必要がある理由です。そうしないと、特定の例外ハンドラーが呼び出されない可能性があります。次の例では、DivisionByZeroException std::exception. から派生

    try{
     // throw an exception (1) 
    }
    catch(const DivisionByZeroException& ex){ .... } // (2) 
    catch(const std::exception& ex{ .... } // (3) 
    catch(...){ .... } // (4) 
    }
    

    この場合、DivisionByZeroException (2) は、(1) 行でスローされた例外を処理するために最初に使用されます。特定のハンドラーが機能しない場合、std::exception から派生したすべての例外 (3) は次の行に引っ掛かります。最後の例外ハンドラーには省略記号 (4) があるため、すべての例外をキャッチできます。

    次は?

    約束通り、C++ における定数と不変性に関する 5 つのルールについて、次の投稿で書きます。