Cでのエラー管理のためのgotoの有効な使用?

FWIF、質問の例で指定したエラー処理イディオムは、これまでの回答で指定されたどの選択肢よりも読みやすく、理解しやすいことがわかりました。 goto の間 これは一般的に悪い考えですが、シンプルで統一された方法で実行すると、エラー処理に役立ちます。この状況では goto でも 、明確に定義され、多かれ少なかれ構造化された方法で使用されています。


原則として、goto を回避することは良い考えですが、ダイクストラが最初に「GOTO は有害であると見なされる」を書いたときに蔓延していた悪用は、最近では選択肢としてほとんどの人々の頭に浮かぶことさえありません。

あなたが概説しているのは、エラー処理の問題に対する一般化可能な解決策です。慎重に使用されている限り、私には問題ありません.

特定の例は次のように簡略化できます (ステップ 1):

int foo(int bar)
{
    int return_value = 0;
    if (!do_something(bar)) {
        goto error_1;
    }
    if (!init_stuff(bar)) {
        goto error_2;
    }
    if (prepare_stuff(bar))
    {
        return_value = do_the_thing(bar);
        cleanup_3();
    }
error_2:
    cleanup_2();
error_1:
    cleanup_1();
    return return_value;
}

プロセスの続行:

int foo(int bar)
{
    int return_value = 0;
    if (do_something(bar))
    {   
        if (init_stuff(bar))
        {
            if (prepare_stuff(bar))
            {
                return_value = do_the_thing(bar);
                cleanup_3();
            }
            cleanup_2();
        }
        cleanup_1();
    }
    return return_value;
}

これは元のコードと同等だと思います。元のコード自体が非常にクリーンでよく整理されているため、これは特にクリーンに見えます。多くの場合、コード フラグメントはそれほど整頓されていません (ただし、整頓されるべきであるという意見は受け入れます)。たとえば、初期化 (セットアップ) ルーチンに渡される状態は、示されているよりも多いため、クリーンアップ ルーチンにも渡される状態が多くなります。


誰もこの代替案を提案していないことに驚いたので、質問はしばらく前からありましたが、追加します。この問題に対処する良い方法の 1 つは、変数を使用して現在の状態を追跡することです。これは goto かどうかに関係なく使用できるテクニックです。 クリーンアップコードに到達するために使用されます。他のコーディング手法と同様に、長所と短所があり、すべての状況に適しているわけではありませんが、スタイルを選択する場合は検討する価値があります - 特に goto を避けたい場合 深くネストされた if で終わることなく

基本的な考え方は、実行する必要がある可能性のあるすべてのクリーンアップ アクションに対して、クリーンアップを実行する必要があるかどうかを判断できる変数が存在するということです。

goto を表示します 最初のバージョンは、元の質問のコードに近いためです。

int foo(int bar)
{
    int return_value = 0;
    int something_done = 0;
    int stuff_inited = 0;
    int stuff_prepared = 0;


    /*
     * Prepare
     */
    if (do_something(bar)) {
        something_done = 1;
    } else {
        goto cleanup;
    }

    if (init_stuff(bar)) {
        stuff_inited = 1;
    } else {
        goto cleanup;
    }

    if (prepare_stuff(bar)) {
        stufF_prepared = 1;
    } else {
        goto cleanup;
    }

    /*
     * Do the thing
     */
    return_value = do_the_thing(bar);

    /*
     * Clean up
     */
cleanup:
    if (stuff_prepared) {
        unprepare_stuff();
    }

    if (stuff_inited) {
        uninit_stuff();
    }

    if (something_done) {
        undo_something();
    }

    return return_value;
}

他のいくつかの手法に対するこの利点の 1 つは、初期化関数の順序が変更された場合でも、正しいクリーンアップが行われることです。たとえば、switch を使用します。 別の回答で説明されている方法、初期化の順序が変更された場合、 switch 最初に実際に初期化されていないものをクリーンアップしようとしないように、非常に慎重に編集する必要があります。

さて、この方法は余分な変数を大量に追加すると主張する人もいるかもしれませんが、実際には、既存の変数が必要な状態をすでに追跡しているか、追跡するように作成できることがよくあります。たとえば、 prepare_stuff() の場合 実際には malloc() の呼び出しです 、または open() まで の場合、返されたポインタまたはファイル記述子を保持する変数を使用できます。例:

int fd = -1;

....

fd = open(...);
if (fd == -1) {
    goto cleanup;
}

...

cleanup:

if (fd != -1) {
    close(fd);
}

ここで、さらに変数を使用してエラー ステータスを追跡すると、goto を回避できます。 初期化が必要なほどインデントが深くなることなく、正しくクリーンアップできます:

int foo(int bar)
{
    int return_value = 0;
    int something_done = 0;
    int stuff_inited = 0;
    int stuff_prepared = 0;
    int oksofar = 1;


    /*
     * Prepare
     */
    if (oksofar) {  /* NB This "if" statement is optional (it always executes) but included for consistency */
        if (do_something(bar)) {
            something_done = 1;
        } else {
            oksofar = 0;
        }
    }

    if (oksofar) {
        if (init_stuff(bar)) {
            stuff_inited = 1;
        } else {
            oksofar = 0;
        }
    }

    if (oksofar) {
        if (prepare_stuff(bar)) {
            stuff_prepared = 1;
        } else {
            oksofar = 0;
        }
    }

    /*
     * Do the thing
     */
    if (oksofar) {
        return_value = do_the_thing(bar);
    }

    /*
     * Clean up
     */
    if (stuff_prepared) {
        unprepare_stuff();
    }

    if (stuff_inited) {
        uninit_stuff();
    }

    if (something_done) {
        undo_something();
    }

    return return_value;
}

繰り返しますが、これには潜在的な批判があります:

  • これらの「if」はパフォーマンスを低下させませんか?いいえ - 成功した場合は、とにかくすべてのチェックを行う必要があるためです (そうしないと、すべてのエラー ケースをチェックするわけではありません)。失敗した場合、ほとんどのコンパイラは失敗した if (oksofar) のシーケンスを最適化します クリーンアップ コードへの 1 回のジャンプをチェックします (GCC は確かにそうします) - いずれにせよ、通常、エラー ケースはパフォーマンスにとってそれほど重要ではありません。
  • <リ>

    これはさらに別の変数を追加していませんか?この場合はい、しかししばしば return_value 変数は、oksofar という役割を果たすために使用できます。 がここで遊んでいます。一貫した方法でエラーを返すように関数を構成すると、2 番目の if を回避することさえできます。 それぞれの場合:

    int return_value = 0;
    
    if (!return_value) {
        return_value = do_something(bar);
    }
    
    if (!return_value) {
        return_value = init_stuff(bar);
    }
    
    if (!return_value) {
        return_value = prepare_stuff(bar);
    }
    

    このようなコーディングの利点の 1 つは、一貫性が意味することは、元のプログラマーが戻り値をチェックするのを忘れた場所が親指のように突き出て、(その 1 つのクラスの) バグを見つけやすくなることです。

つまり、これは (まだ) この問題を解決するために使用できるもう 1 つのスタイルです。正しく使用すると、非常にクリーンで一貫性のあるコードが可能になります。また、他の手法と同様に、悪意のある人の手に渡れば、長くて混乱を招くコードが生成される可能性があります :-)