101

この質問は、実際には、programming.reddit.com でしばらく前に行われた興味深い議論の結果です。基本的には、次のコードに要約されます。

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 )) {
        goto error_3;
    }
    return_value = do_the_thing( bar );
error_3:
    cleanup_3();
error_2:
    cleanup_2();
error_1:
    cleanup_1();
    return return_value;
}

ここでの使用はgoto最善の方法であるように思われ、あらゆる可能性の中で最もクリーンで効率的なコードが得られます。少なくとも私にはそう思われます。Code Completeでの Steve McConnell の引用:

goto は、リソースを割り当て、それらのリソースに対して操作を実行し、リソースの割り当てを解除するルーチンで役立ちます。goto を使用すると、コードの 1 つのセクションでクリーンアップできます。goto は、エラーを検出した各場所でリソースの割り当てを解除するのを忘れる可能性を減らします。

このアプローチの別のサポートは、このセクションのLinux Device Drivers bookから得られます。

どう思いますか?gotoこの場合、 C での有効な使用法はありますか? より複雑で効率の悪いコードを生成する他の方法を好みますが、回避しgotoますか?

4

16 に答える 16

70

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

于 2009-04-26T05:05:34.880 に答える
20

原則として、goto を回避することは良い考えですが、Dijkstra が最初に「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;
}

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

于 2009-04-25T14:13:03.887 に答える
16

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

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

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)チェックのシーケンスをクリーンアップコードへの単一のジャンプに最適化します (GCC は確かにそうします) - いずれにせよ、エラーの場合は通常、パフォーマンスにとってそれほど重要ではありません。
  • これはさらに別の変数を追加していませんか? この場合はそうですが、多くの場合、ここreturn_valueで果たしている役割を果たすために変数を使用できoksofarます。一貫した方法でエラーを返すように関数を構成するとif、それぞれのケースで 2 番目のエラーを回避することもできます。

    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 つのスタイルです。正しく使用すると、非常にクリーンで一貫性のあるコードが可能になります。他の手法と同様に、悪意のある人の手に渡れば、長くて混乱を招くコードが生成される可能性があります:-)

于 2011-06-14T15:27:22.040 に答える
9

gotoキーワードの問題は、ほとんど誤解されています。それは単純な悪ではありません。goto ごとに追加の制御パスを作成することに注意する必要があります。コードについて推論することが難しくなり、その結果、その有効性が低下します。

FWIW、developer.apple.com チュートリアルを調べると、エラー処理に goto アプローチが採用されています。

goto は使用しません。戻り値はより重要です。例外処理はsetjmp/longjmp-- できる限りの方法で行われます。

于 2009-04-25T13:25:21.023 に答える
4

(void)* ポインターに道徳的に問題があるのと同様に、goto ステートメントについても道徳的に問題はありません。

それはすべて、ツールをどのように使用するかにかかっています。あなたが提示した(些細な)ケースでは、オーバーヘッドは増えますが、caseステートメントは同じロジックを実現できます。本当の問題は、「私の速度要件は何ですか?」ということです。

特に短いジャンプにコンパイルされるように注意している場合は特に、 goto は単純に高速です。速度が重視されるアプリケーションに最適です。他のアプリケーションの場合、保守性のために if/else + case でオーバーヘッド ヒットを取ることはおそらく理にかなっています。

覚えておいてください: goto はアプリケーションを強制終了しません。開発者はアプリケーションを強制終了します。

更新:これが事例です

int foo(int bar) { 
     int return_value = 0 ; 
     int failure_value = 0 ;

     if (!do_something(bar)) { 
          failure_value = 1; 
      } else if (!init_stuff(bar)) { 
          failure_value = 2; 
      } else if (prepare_stuff(bar)) { 
          return_value = do_the_thing(bar); 
          cleanup_3(); 
      } 

      switch (failure_value) { 
          case 2: cleanup_2(); 
          case 1: cleanup_1(); 
          default: break ; 
      } 
} 
于 2009-04-25T13:32:41.770 に答える
3

GOTOが便利です。これはプロセッサが実行できることであり、これがアクセスできる必要がある理由です。

関数に少し何かを追加したい場合がありますが、単一の goto を使用すると簡単に実行できます。時間を節約できます..

于 2009-04-26T01:09:20.940 に答える
1

私は個人的に「セーフティ クリティカル コードを書くための 10 の法則 - 10 の法則」の信奉者です。

そのテキストから、goto についての良いアイデアであると私が信じていることを示す小さなスニペットを含めます。


規則: すべてのコードを非常に単純な制御フロー構造に制限します。goto ステートメント、setjmp または longjmp 構造、および直接または間接再帰を使用しないでください。

理論的根拠: 単純な制御フローは、検証のためのより強力な機能に変換され、多くの場合、コードの明瞭さが向上します。ここでおそらく最大の驚きは、再帰の追放です。ただし、再帰がなければ、非循環関数呼び出しグラフが保証されます。これは、コード アナライザーによって悪用される可能性があり、制限されるべきすべての実行が実際に制限されていることを証明するのに直接役立ちます。(この規則では、すべての関数が単一の戻り点を持つ必要はないことに注意してください。ただし、これにより制御フローが簡素化されることもよくあります。ただし、初期のエラー戻りがより簡単な解決策である場合は十分あります。)


goto の使用を禁止するのは悪いように思えますが、

ルールが最初は厳格に見える場合は、文字通りあなたの人生がその正確さに依存する可能性があるコードをチェックできるようにすることを念頭に置いてください: あなたが飛ぶ飛行機、原子力発電所を制御するために使用されるコードあなたが住んでいる場所から数マイル、または宇宙飛行士を軌道に乗せる宇宙船。ルールは車のシートベルトのような役割を果たします。最初は少し不快かもしれませんが、しばらくすると使用することが当たり前になり、使用しないことは想像を絶するものになります。

于 2009-04-27T19:53:49.887 に答える
1

一般に、コードの一部を最も明確に記述できるという事実は、プログラム フローが一般的に望ましいよりも複雑である可能性が高いgotoという兆候を示すものと見なします。の使用を避けるために他のプログラム構造を奇妙な方法で組み合わせることgotoは、病気ではなく症状を治療しようとすることになります。あなたの特定の例は、なしで実装するのはそれほど難しくないかもしれませんgoto:

  行う {
    .. 早期終了の場合にのみクリーンアップが必要になる thing1 をセットアップします
    if (エラー) ブレーク;
    行う
    {
      .. 早期終了の場合にクリーンアップが必要な thing2 をセットアップします
      if (エラー) ブレーク;
      // ***** この行に関するテキストを参照
    } while(0);
    .. クリーンアップの事 2;
  } while(0);
  .. クリーンアップの事 1;

ただし、クリーンアップが関数が失敗したときにのみ発生することになっている場合は、最初のターゲット ラベルの直前にgotoa を配置することで処理できます。return上記のコードではreturn、 でマークされた行に を追加する必要があり*****ます。

「通常の場合でもクリーンアップする」シナリオでは、ターゲット ラベル自体がand /構成よりもはるかに「LOOK AT ME」と実際に叫ぶため、とりわけ/構成よりも を使用するgoto方が明確であると考えます。「エラーの場合のみクリーンアップ」の場合、ステートメントは、読みやすさの観点から考えられる最悪の場所に配置する必要があります (return ステートメントは、通常、関数の先頭か、「見える」場所に配置する必要があります)。終わり); ターゲット ラベルの直前にあると、「ループ」の終了直前にあるよりもはるかに容易にその条件を満たすことができます。dowhile(0)breakdowhile(0)returnreturn

ところで、私が時々gotoエラー処理に使用する 1 つのシナリオはswitch、複数のケースのコードが同じエラー コードを共有する場合のステートメント内です。私のコンパイラは多くの場合、複数のケースが同じコードで終わることを認識するのに十分スマートですが、次のように言う方が明確だと思います:

REPARSE_PACKET:
  スイッチ(パケット[0])
  {
    ケース PKT_THIS_OPERATION:
      if (問題の状態)
        PACKET_ERROR に移動します。
      ... THIS_OPERATION を処理する
      壊す;
    ケース PKT_THAT_OPERATION:
      if (問題の状態)
        PACKET_ERROR に移動します。
      ... THAT_OPERATION を処理する
      壊す;
    ...
    ケース PKT_PROCESS_CONDITIONALLY
      場合 (packet_length < 9)
        PACKET_ERROR に移動します。
      if (パケット[4]を含むpacket_condition)
      {
        packet_length -= 5;
        memmove(パケット、パケット+5、パケット長);
        REPARSE_PACKET に移動します。
      }
      そうしないと
      {
        パケット[0] = PKT_CONDITION_SKIPPED;
        パケット[4] = packet_length;
        パケット長 = 5;
        packet_status = READY_TO_SEND;
      }
      壊す;
    ...
    デフォルト:
    {
     パケット_エラー:
      packet_error_count++;
      パケット長 = 4;
      パケット[0] = PKT_ERROR;
      packet_status = READY_TO_SEND;
      壊す;
    }
  }   

gotoステートメントをに置き換えることはできますが、ラップされた条件付き実行パケットを処理するために/ループ{handle_error(); break;}を使用することもできますが、.を使用するよりも明確だとは思いません。さらに、 が使用されているすべての場所からコードをコピーすることは可能かもしれませんが、コンパイラーは複製されたコードを一度書き出して、ほとんどの出現箇所をその共有コピーへのジャンプに置き換えるかもしれませんが、 を使用すると場所に気づきやすくなります。これにより、パケットの設定が少し異なります(たとえば、「条件付きで実行」命令が実行しないことを決定した場合)。dowhile(0)continuegotoPACKET_ERRORgoto PACKET_ERRORgoto

于 2012-06-06T16:25:56.123 に答える
0

ここでの質問は、与えられたコードに関して誤っていると思います。

検討:

  1. do_something()、init_stuff()、および prepare_stuff() は、失敗した場合に false または nil を返すため、失敗したかどうかを認識しているように見えます。
  2. foo() で直接設定される状態がないため、状態を設定する責任はこれらの関数の責任のようです。

したがって、do_something()、init_stuff()、および prepare_stuff() は、独自の cleanup を実行する必要があります。do_something() の後にクリーンアップする別の cleanup_1() 関数を持つことは、カプセル化の哲学を壊します。悪いデザインです。

彼らが独自のクリーンアップを行った場合、 foo() は非常に単純になります。

一方で。foo() が実際に破棄する必要のある独自の状態を作成した場合は、goto が適切です。

于 2010-05-29T07:50:45.720 に答える
0

cleanup_3クリーンアップを行ってから を呼び出す必要があるように思えますcleanup_2。同様に、cleanup_2クリーンアップを実行してから、cleanup_1 を呼び出す必要があります。を行うときはいつでもcleanup_[n]、それcleanup_[n-1]が必要であるように見えます。したがって、それはメソッドの責任である必要があります(たとえば、を呼び出してリークを引き起こす可能性cleanup_3なしに呼び出すことはできません)。cleanup_2

そのアプローチを考えると、goto の代わりに、単純にクリーンアップ ルーチンを呼び出してから戻ることになります。

このgotoアプローチは間違っているわけでも悪いわけでもありませんが、必ずしも「最もクリーンな」アプローチ (IMHO) ではないことに注意してください。

最適なパフォーマンスを求めるなら、このgotoソリューションが最適だと思います。ただし、パフォーマンスが重要な一部のアプリケーション (デバイス ドライバー、組み込みデバイスなど) にのみ関連すると予想しています。それ以外の場合は、コードの明確さよりも優先度の低いマイクロ最適化です。

于 2009-04-25T13:49:09.157 に答える
-1

次の例で説明されている手法を使用することを好みます...

struct lnode *insert(char *data, int len, struct lnode *list) {
    struct lnode *p, *q;
    uint8_t good;
    struct {
            uint8_t alloc_node : 1;
            uint8_t alloc_str : 1;
    } cleanup = { 0, 0 };

    // allocate node.
    p = (struct lnode *)malloc(sizeof(struct lnode));
    good = cleanup.alloc_node = (p != NULL);

    // good? then allocate str
    if (good) {
            p->str = (char *)malloc(sizeof(char)*len);
            good = cleanup.alloc_str = (p->str != NULL);
    }

    // good? copy data
    if(good) {
            memcpy ( p->str, data, len );
    }

    // still good? insert in list
    if(good) {
            if(NULL == list) {
                    p->next = NULL;
                    list = p;
            } else {
                    q = list;
                    while(q->next != NULL && good) {
                            // duplicate found--not good
                            good = (strcmp(q->str,p->str) != 0);
                            q = q->next;
                    }
                    if (good) {
                            p->next = q->next;
                            q->next = p;
                    }
            }
    }

    // not-good? cleanup.
    if(!good) {
            if(cleanup.alloc_str)   free(p->str);
            if(cleanup.alloc_node)  free(p);
    }

    // good? return list or else return NULL
    return (good? list: NULL);

}

ソース: http://blog.staila.com/?p=114

于 2011-11-17T19:51:23.880 に答える