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


92

この質問は、実際には、少し前に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最善の方法であるように見え、すべての可能性の中で最もクリーンで最も効率的なコードが得られるか、少なくとも私には思えます。コード・コンプリートでのスティーブ・マコーネルの引用:

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

このアプローチの別のサポートは、このセクションのLinux Device Driversブックから来ています。

どう思いますか?このケースgotoはCでの有効な使用ですか?より複雑なコードや効率の悪いコードを生成する他の方法を好みgotoますか?



@Eli:タグを削除して、関数(cleanup_3();)をifの括弧に入れてみませんか?

@秋戸:どういう意味ですか?完全なコードサンプルを使用して、提案を回答として投稿できますか?
Eli Bendersky

@EliBendersky:私の答えを見てください。

Visual Basic(VBSおよびVB6を含む)で私が最も嫌っていたのは、on error goto errorエラー処理システムでした:)
Manu343726

回答:


61

FWIF、私はあなたが質問の例であなたが与えたエラー処理イディオムは、これまでの回答で与えられたどの代替案よりも読みやすく、理解しやすいと思います。一方でgoto、一般的に悪い考えですシンプルかつ均一に行われたとき、それはエラー処理のために有用であることができます。この状況では、それはですがgoto、明確に定義され、多かれ少なかれ構造化された方法で使用されています。


1
彼はそれらのタグを削除して関数を直接ifに入れることはできませんか?

8
@StartupCrazy、私はこれが古いことを知っていますが、このサイトへの投稿の妥当性のために、私は彼ができないことを指摘します。彼のコードのgoto error3でエラーが発生した場合、クリーンアップ1、2、3を実行します。推奨されるソリューションでは、クリーンアップ3のみを実行します。入れ子にすることもできますが、それは単なるアンチパターンであり、プログラマーは避ける必要があります。
gbtimmon

17

一般的なルールとして、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;
}

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


23
はい、ネストされたソリューションは実行可能な代替策の1つです。残念ながら、ネストレベルが深くなると、実行可能性が低くなります。
Eli Bendersky 2009

4
@eliben:同意します-しかし、より深い入れ子は、(おそらくそうです)より多くの関数を導入する必要があること、または準備ステップでより多くのことを行う必要があること、またはコードをリファクタリングする必要があることを示す可能性があります。各準備関数はセットアップを実行し、チェーンの次の関数を呼び出し、独自のクリーンアップを実行する必要があると主張できます。それはその作業をローカライズします。3つのクリーンアップ機能を節約することもできます。また、一部は、他の呼び出しシーケンスでセットアップまたはクリーンアップ機能が使用されている(使用可能である)かどうかにも依存します。
ジョナサンレフラー、

6
残念ながら、これはループでは機能しません-ループ内でエラーが発生した場合、gotoはフラグと 'break'ステートメントを設定およびチェックする方法よりもはるかにクリーンです(これは巧妙に偽装されたgotoです)。
Adam Rosenfield、

1
@スミス、防弾チョッキなしで運転するような感じ。
ストレージャー、2009

6
私はここでネクロマンシングしていることを知っていますが、このアドバイスはかなり貧弱です- 矢のアンチパターンは避けるべきです。
KingRadical 2013年

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つのスタイルです。正しく使用すると、非常にクリーンで一貫性のあるコードが可能になります。また、他のテクニックと同様に、悪意のあるユーザーにとっては、時間がかかり混乱するコードを生成してしまう可能性があります:-)


2
あなたはパーティーに遅れているように見えますが、私は確かに答えが好きです!

Linusはおそらくあなたのコードを拒否しますblogs.oracle.com/oswald/entry/is_goto_the_root_of
Fizz

1
@ user3588161:彼がそうする場合、それは彼の特権です-しかし、あなたがリンクした記事に基づいて、それが事実であるかどうかはわかりません:私が説明しているスタイルでは、(1)条件式はネ​​ストしていません任意に深く、(2)とにかく必要なものと比較して、追加の「if」ステートメントはありません(すべての戻りコードをチェックする場合)。
psmears 2014

恐ろしい後藤の代わりに、これだけでなく、さらに悪い矢印のアンチパターンソリューション!
常磁性クロワッサン2015年

8

gotoキーワードの問題はほとんど誤解されています。それは明白な悪ではありません。すべてのgotoで作成する追加の制御パスに注意する必要があります。コードを推論し、その妥当性を判断するのは難しくなります。

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

後藤は使用しません。より重要なのは戻り値です。例外処理は、setjmp/longjmp少しでも可能です。


8
私は確かにsetjmp / longjmpが要求されたいくつかのケースで使用しましたが、それはgotoよりも "さらに悪い"とさえ考えます(私が使用するのは、要求されたときに多少控えめですが)。setjmp / longjmpを使用するのは、(1)ターゲットが現在の状態に影響されない方法ですべてをシャットダウンする場合、または(2)ターゲットが内部で制御されるすべてを再初期化する場合のみです。現在の状態に依存しない方法でsetjmp / longjmp-guardedブロック。
スーパーキャット2011

4

gotoステートメントには、(void)*ポインタに道徳的な誤りがある以上、道徳的に間違っているものはありません。

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

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

覚えておいてください: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 ; 
      } 
} 

1
「ケース」の代替案を提示できますか?また、これはvoid *とはかなり異なると主張します。これは、Cでの深刻なデータ構造の抽象化に必要です。void*に真剣に反対する人はいないと思います。1つの大きなコードベースがなければ、1つの大きなコードベースは見つかりません。それ。
Eli Bendersky 2009

Re:void *、それはまさに私の主張ですが、どちらにも道徳的に問題はありません。以下のスイッチ/ケースの例。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(); }スイッチ(failure_value){ケース2:cleanup_2(); 休憩; ケース1:cleanup_1(); 休憩; デフォルト:break; }}
webmarc 2009

5
@webmarc、申し訳ありませんが、これは恐ろしいことです!ラベル付きのgotoを完全にシミュレートしただけです-ラベルに独自の非説明的な値を作成し、スイッチ/ケースでgotoを実装します。Failure_value = 1は "goto cleanup_something"よりもクリーンですか?
Eli Bendersky 2009

4
ここに私を設置したような気がします...あなたの質問は意見です。しかし、私が答えを出したとき、それは恐ろしいことです。:-(ラベル名についてのあなたの不満については、それらはあなたの例の残りの部分と同じように説明的です:cleanup_1、foo、bar。なぜそれが質問に密接に関係していないのにラベル名を攻撃しているのですか?
webmarc

1
「セットアップ」するつもりはなく、ネガティブな感情を引き起こしてしまいました。新しいアプローチは「gotoの削除」のみを対象としており、明確さは増していません。gotoの機能を再実装したかのように、わかりにくいコードを多く使用しています。このIMHOは良いことではありません-gotoを取り除くためだけにgotoを削除します。
Eli Bendersky 2009

2

GOTOは便利です。それはあなたのプロセッサができることであり、これがあなたがそれにアクセスするべきである理由です。

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


3
プロセッサが実行できるすべてのことを行う必要はありません。ほとんどの場合、gotoは他の方法よりも混乱します。
David Thornley、

@DavidThornley:はい、プロセッサが実行できるすべてのことへのアクセス必要です。そうでなければ、プロセッサを浪費しています。後藤はプログラミングで最高の命令です。
Ron Maimon、2015

1

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

  行う {
    ..早期終了の場合にのみクリーンアップが必要になるthing1を設定
    (エラー)が壊れた場合
    行う
    {
      ..早期終了の場合にクリーンアップが必要になるthing2を設定します
      (エラー)が壊れた場合
      // *****この行に関するテキストを見る
    } while(0);
    ..クリーンアップthing2;
  } while(0);
  ..クリーンアップthing1;

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

「通常の場合でもクリーンアップ」のシナリオでは、ターゲットラベル自体が実際に「/ LOOK AT ME」をand / 構成よりもはるかに強く叫ぶため、/ 構成gotoよりも明確に使用すると見なします。「エラーの場合のみクリーンアップ」の場合、文は読みやすさの観点から考えて、最悪の場所に配置する必要があります(通常、return文は関数の先頭か、「見た目」のどちらかでなければなりません。終わり); 持つターゲットラベルは、単に「ループ」の終了前に1つを有するよりも、はるかに容易にその資格を満たしている直前に。dowhile(0)breakdowhile(0)returnreturn

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

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

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


1

私は個人的に「The Safety of Ten-10 Rules for Writing Safety Critical Code」のフォロワーです。

gotoについて良い考えだと私が信じるものを説明する、そのテキストからの小さなスニペットを含めます。


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

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


gotoの使用を禁止するのは悪いようですが

ルールが最初はドラコニアンのように思える場合、これらのルールは、文字通りあなたの人生がその正確さに依存する可能性があるコードをチェックできるようにすることを意図していることを覚えておいてください:あなたが飛行する飛行機、原子力発電所を制御するために使用されるコードあなたが住んでいる場所から数マイル、または宇宙飛行士を軌道に乗せる宇宙船。ルールは車のシートベルトのように機能します。最初はおそらく少し不快ですが、しばらくすると使用が自然になり、使用しないことは想像できなくなります。


22
これの問題は、完全に追放する通常の方法はgoto、深くネストされたifまたはループで「賢い」ブール値のセットを使用することです。それは本当に助けにはなりません。たぶんあなたのツールはそれをより良くするでしょう、しかしあなたはそうしないでしょう、そしてあなたはより重要です。
ドナルフェロー

1

質問に記載されている逆順でのgotoクリーンアップは、ほとんどの機能でクリーンアップする最もクリーンな方法であることに同意します。しかし、私はまた、とにかく関数をクリーンアップしたい場合があることも指摘しておきたいと思います。これらのケースで、if(0){label:}イディオムがクリーンアッププロセスの適切なポイントに移動する場合、次のバリアントを使用します。

int decode ( char * path_in , char * path_out )
{
  FILE * in , * out ;
  code c ;
  int len ;
  int res = 0  ;
  if ( path_in == NULL )
    in = stdin ;
  else
    {
      if ( ( in = fopen ( path_in , "r" ) ) == NULL )
        goto error_open_file_in ;
    }
  if ( path_out == NULL )
    out = stdout ;
  else
    {
      if ( ( out = fopen ( path_out , "w" ) ) == NULL )
        goto error_open_file_out ;
    }

  if( read_code ( in , & c , & longueur ) )
    goto error_code_construction ;

  if ( decode_h ( in , c , out , longueur ) )
  goto error_decode ;

  if ( 0 ) { error_decode: res = 1 ;}
  free_code ( c ) ;
  if ( 0 ) { error_code_construction: res = 1 ; }
  if ( out != stdout ) fclose ( stdout ) ;
  if ( 0 ) { error_open_file_out: res = 1 ; }
  if ( in != stdin ) fclose ( in ) ;
  if ( 0 ) { error_open_file_in: res = 1 ; }
  return res ;
 }

0

cleanup_3クリーンアップを実行する必要があるようですが、を呼び出しますcleanup_2。同様に、cleanup_2クリーンアップを実行してから、cleanup_1を呼び出す必要があります。あなたがそのいつでも表示されますcleanup_[n]cleanup_[n-1]したがって、メソッドの責任である必要があり、必要とされるが(ように、例えば、cleanup_3呼び出すことなく呼び出されることは決してできないcleanup_2と、おそらく漏れの原因となります。)

このアプローチでは、gotosの代わりに、クリーンアップルーチンを呼び出してから戻ります。

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

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


4
それはうまくいきません-クリーンアップはリソースに固有であり、このルーチンでのみこの順序で割り当てられます。他の場所では、それらは関連していません。そのため、他から呼び出しても意味がありません。
Eli Bendersky 2009

0

ここでの質問は、与えられたコードに関しては間違いです。

検討してください:

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

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

独自のクリーンアップを行うと、foo()は非常に単純になります。

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


0

これが私が好んだものです:

bool do_something(void **ptr1, void **ptr2)
{
    if (!ptr1 || !ptr2) {
        err("Missing arguments");
        return false;
    }
    bool ret = false;

    //Pointers must be initialized as NULL
    void *some_pointer = NULL, *another_pointer = NULL;

    if (allocate_some_stuff(&some_pointer) != STUFF_OK) {
        err("allocate_some_stuff step1 failed, abort");
        goto out;
    }
    if (allocate_some_stuff(&another_pointer) != STUFF_OK) {
        err("allocate_some_stuff step 2 failed, abort");
        goto out;
    }

    void *some_temporary_malloc = malloc(1000);

    //Do something with the data here
    info("do_something OK");

    ret = true;

    // Assign outputs only on success so we don't end up with
    // dangling pointers
    *ptr1 = some_pointer;
    *ptr2 = another_pointer;
out:
    if (!ret) {
        //We are returning an error, clean up everything
        //deallocate_some_stuff is a NO-OP if pointer is NULL
        deallocate_some_stuff(some_pointer);
        deallocate_some_stuff(another_pointer);
    }
    //this needs to be freed every time
    free(some_temporary_malloc);
    return ret;
}

0

しかし、古い議論.... "矢印アンチパターン"を使用して、後ですべてのネストされたレベルを静的インライン関数にカプセル化するのはどうでしょうか。コードはきれいに見え、最適化され(最適化が有効な場合)、gotoは使用されません。つまり、分割して征服します。以下の例:

static inline int foo_2(int bar)
{
    int return_value = 0;
    if ( prepare_stuff( bar ) ) {
        return_value = do_the_thing( bar );
    }
    cleanup_3();
    return return_value;
}

static inline int foo_1(int bar)
{
    int return_value = 0;
    if ( init_stuff( bar ) ) {
        return_value = foo_2(bar);
    }
    cleanup_2();
    return return_value;
}

int foo(int bar)
{
    int return_value = 0;
    if (do_something(bar)) {
        return_value = foo_1(bar);
    }
    cleanup_1();
    return return_value;
}

スペースに関しては、スタックに3倍の変数を作成していますが、これは適切ではありませんが、この単純な例で変数をスタックから削除し、レジスタを使用して-O2でコンパイルすると、これは消えます。私が上記のブロックから得gcc -S -O2 test.cたのは以下です:

    .section    __TEXT,__text,regular,pure_instructions
    .macosx_version_min 10, 13
    .globl  _foo                    ## -- Begin function foo
    .p2align    4, 0x90
_foo:                                   ## @foo
    .cfi_startproc
## %bb.0:
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register %rbp
    pushq   %r14
    pushq   %rbx
    .cfi_offset %rbx, -32
    .cfi_offset %r14, -24
    movl    %edi, %ebx
    xorl    %r14d, %r14d
    xorl    %eax, %eax
    callq   _do_something
    testl   %eax, %eax
    je  LBB0_6
## %bb.1:
    xorl    %r14d, %r14d
    xorl    %eax, %eax
    movl    %ebx, %edi
    callq   _init_stuff
    testl   %eax, %eax
    je  LBB0_5
## %bb.2:
    xorl    %r14d, %r14d
    xorl    %eax, %eax
    movl    %ebx, %edi
    callq   _prepare_stuff
    testl   %eax, %eax
    je  LBB0_4
## %bb.3:
    xorl    %eax, %eax
    movl    %ebx, %edi
    callq   _do_the_thing
    movl    %eax, %r14d
LBB0_4:
    xorl    %eax, %eax
    callq   _cleanup_3
LBB0_5:
    xorl    %eax, %eax
    callq   _cleanup_2
LBB0_6:
    xorl    %eax, %eax
    callq   _cleanup_1
    movl    %r14d, %eax
    popq    %rbx
    popq    %r14
    popq    %rbp
    retq
    .cfi_endproc
                                        ## -- End function

.subsections_via_symbols

-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


2
フラグの付いたコードと矢印のアンチパターン(どちらも例で紹介されています)はどちらも、コードを不必要に複雑にするものです。「goto is evil」以外にそれらを使用する正当な理由はありません。
KingRadical、2013年

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.