web-dev-qa-db-ja.com

これはCでのgotoの適切なユースケースですか?

私はこれについて質問するのをためらいます。「討論、議論、投票、または議論の懇願」はしたくないのですが、私はCに不慣れで、言語で使用される一般的なパターンについてさらに洞察を得たいと思っています。

最近、gotoコマンドの嫌悪感を聞いたが、最近、まともな使用例も見つけた。

このようなコード:

error = function_that_could_fail_1();
if (!error) {
    error = function_that_could_fail_2();
    if (!error) {
        error = function_that_could_fail_3();
        ...to the n-th tab level!
    } else {
        // deal with error, clean up, and return error code
    }
} else {
    // deal with error, clean up, and return error code
}

クリーンアップ部分がすべて非常に似ている場合、次のように少しきれいに書くことができます(私の意見は?)。

error = function_that_could_fail_1();
if(error) {
    goto cleanup;
}
error = function_that_could_fail_2();
if(error) {
    goto cleanup;
}
error = function_that_could_fail_3();
if(error) {
    goto cleanup;
}
...
cleanup:
// deal with error if it exists, clean up
// return error code

これはCでのgotoの一般的または許容可能なユースケースですか?これを行う別の/より良い方法はありますか?

60
Robz

gotoステートメント(およびそれに対応するラベル)は、フロー制御プリミティブです(ステートメントの条件付き実行とともに)。つまり、プログラムフロー制御ネットワークを構築できるようにするためのものです。これらは、フローチャートのノード間の矢印をモデル化したものと考えることができます。

これらの一部は、直接線形フローがある場合にすぐに最適化できます(基本的なステートメントのシーケンスを使用するだけです)。他のパターンは、それらが利用可能な場合、構造化プログラミング構造に置き換えるのが最適です。 whileループのように見える場合はwhileループを使用、OK?構造化プログラミングパターンは、少なくともgotoステートメントの混乱よりも、少なくとも潜在的に意図が明確です。

しかし、Cにはすべての可能な構造化プログラミング構造が含まれているわけではありません。 (関連するすべてがまだ発見されているかどうかははっきりしません。現在、発見の速度は遅いですが、すべてが発見されたと言うのをためらうと思います。)私たちが知っているもののうち、Cには間違いなくtry/catch/finally構造(および例外も)。また、マルチレベルのbreak- from-loopもありません。これらは、gotoを使用して実装できるものです。他のスキームを使用してこれらを実行することも可能です。Cには十分gotoプリミティブのセットがあることがわかっています—しかし、これらには、フラグ変数の作成とはるかに複雑なループが含まれることがよくありますまたはガード条件;コントロール分析とデータ分析の絡み合いを増やすと、プログラム全体の理解が難しくなります。また、コンパイラが最適化したり、CPUが高速に実行したりすることがさらに困難になります(ほとんどのフロー制御構成要素—およびdefinitelygoto —非常に安価です)。

したがって、必要でない限りgotoを使用するべきではありませんが、それが存在すること、そしてmayが必要であること、そしてそれが必要な場合はあまり気にしないことを認識する必要があります悪い。必要な場合の例としては、呼び出された関数がエラー状態を返したときのリソース割り当て解除があります。 (つまり、try/finallyです。)gotoなしでそれを書くことは可能ですが、それを行うと、それを維持するという問題など、それ自体の欠点を持つ可能性があります。ケースの例:

int frobnicateTheThings() {
    char *workingBuffer = malloc(...);
    int i;

    for (i=0 ; i<numberOfThings ; i++) {
        if (giveMeThing(i, workingBuffer) != OK)
            goto error;
        if (processThing(workingBuffer) != OK)
            goto error;
        if (dispatchThing(i, workingBuffer) != OK)
            goto error;
    }

    free(workingBuffer);
    return OK;

  error:
    free(workingBuffer);
    return OOPS;
}

コードはさらに短くてもかまいませんが、要点を示すには十分です。

48
Donal Fellows

はい。

これは、たとえばLinuxカーネルで使用されます。これが ほぼ10年前のスレッド の終わりからのメールです。

投稿者:ロバート・ラブ
件名:Re:2.6.0-test *の可能性はありますか?
日付:2003年1月12日17:58:06 -0500

2003-01-12日曜日の17:22、ロブウィルケンスは次のように書いています。

私は「gotoを使用しないでください」と言い、代わりに「cleanup_lock」関数を使用して、すべてのreturnステートメントの前にそれを追加します。負担にはなりません。はい、それは開発者にもう少し努力するように求めていますが、最終結果はより良いコードです。

いいえ、それはひどく、それはカーネルを膨らませます。終了コードを最後に1回持つのではなく、N個のエラーパスの一連のジャンクをインライン化します。 キャッシュのフットプリントが重要であり、それを強制終了しました。

読みやすくもない。

最後の引数として、通常のスタック風の巻き上げと巻き戻し、つまり、.

        do A
        if (error)
            goto out_a;
        do B
        if (error)
            goto out_b;
        do C
        if (error)
            goto out_c;
        goto out;
        out_c:
        undo C
        out_b:
        undo B:
        out_a:
        undo A
        out:
        return ret;

これを止めて。

ロバート・ラブ

とは言っても、gotoの使用に慣れたらスパゲッティコードを作成しないようにするためには、多くの訓練が必要です。そのため、速度や低メモリフットプリント(カーネルや組み込みシステムなど)を必要とするものを作成しない限り、本当に考えてみてください前に最初のgotoを書きます。

67
Izkata

私の意見では、投稿したコードはgotoの有効な使用例です。これは、下にジャンプするだけで、プリミティブな例外ハンドラーのように使用するだけだからです。

ただし、古いgotoの議論のため、プログラマーはgotoを約40年間避けてきたため、gotoでコードを読み取るために使用されていません。これはgotoを回避する正当な理由です。単に標準ではありません。

私はコードをCプログラマーがより読みやすいものに書き直したでしょう。

Error some_func (void)
{
  Error error;
  type_t* resource = malloc(...);

  error = some_other_func (resource);

  free (resource);

  /* error handling can be placed here, or it can be returned to the caller */

  return error;
}


Error some_other_func (type_t* resource)  // inline if needed
{
  error = function_that_could_fail_1();
  if(error)
  {
    return error;
  }

  /* ... */

  error = function_that_could_fail_2();
  if(error)
  {
    return error;
  }

  /* ... */

  return ok;
}

この設計の利点:

  • 実際の作業を行う関数は、データの割り当てなど、アルゴリズムに関係のないタスクに関与する必要はありません。
  • コードは、Cプログラマにとってgotoとラベルを恐れているので、それほど見た目が悪くなります。
  • エラー処理と割り当て解除を、アルゴリズムを実行する関数の外の同じ場所に集中化できます。関数が独自の結果を処理することは意味がありません。
14
user29079

ドナルドE.クヌース(スタンフォード大学)による 構造化プログラミングとGOTOステートメント の有効な使用例を説明した有名な論文です。この論文は、GOTOの使用が罪と見なされていた時代、および構造化プログラミングの動きがピークに達した時代に掲載されました。 GoToが有害と見なされることを確認することをお勧めします。

11
NoChance

Javaでは、次のようにします。

makeCalls:  {
    error = function_that_could_fail_1();
    if (error) {
        break makeCalls;
    }
    error = function_that_could_fail_2();
    if (error) {
        break makeCalls;
    }
    error = function_that_could_fail_3();
    if (error) {
        break makeCalls;
    }
    ...
    return 0;  // No error code.
}
// deal with error if it exists, clean up
// return error code

よく使います。私がgotoを嫌うのと同じように、他のほとんどのCスタイル言語では、あなたのコードを使用します。それを行うには他に良い方法はありません。 (ネストされたループからのジャンプも同様のケースです。Javaではラベル付きのbreakを使用し、それ以外の場所ではgotoを使用します。)

9
RalphChapin

私はそれはisまともなユースケースだと思いますが、「エラー」がブール値に過ぎない場合、あなたが望むものを達成するための別の方法があります:

error = function_that_could_fail_1();
error = error || function_that_could_fail_2();
error = error || function_that_could_fail_3();
if(error)
{
     // do cleanup
}

これは、ブール演算子の短絡評価を利用します。これが「より良い」のであれば、あなたの個人的な好みとその慣用法に慣れている方法次第です。

8
Doc Brown

Linuxスタイルガイドでは、例に沿ったgotoを使用する特定の理由を示しています。

https://www.kernel.org/doc/Documentation/process/coding-style.rst

Gotosを使用する根拠は次のとおりです。

  • 無条件のステートメントは理解しやすく、理解しやすい
  • 入れ子が減少します
  • 変更ができない場合に個々の出口点を更新しないことによるエラー
  • コンパイラーの作業を省いて、冗長なコードを最適化します;)

免責事項私は自分の作品を共有することになっています。ここでの例は少し工夫されていますので、クマと一緒に我慢してください。

これはメモリ管理に適しています。私は最近、動的にメモリを割り当てたコード(たとえば、関数によって返される_char *_)に取り組みました。パスを調べて、パスのトークンを解析することにより、パスが有効かどうかを確認する関数:

_tmp_string = strdup(string);
token = strtok(tmp_string,delim);
while( token != NULL ){
    ...
    some statements, some involving dynamically allocated memory
    ...
    if ( check_this() ){
        free(var1);
        free(var2);
        ...
        free(varN);
        return 1;
    }
    ...
    some more stuff
    ...
    if(something()){
        if ( check_that() ){
            free(var1);
            free(var2);
            ...
            free(varN);
            return 1;
        } else {
            free(var1);
            free(var2);
            ...
            free(varN);
            return 0;
        }
    }
    token = strtok(NULL,delim);
}

free(var1);
free(var2);
...
free(varN);
return 1;
_

ここで、_varNplus1_を追加する必要がある場合は、次のコードの方がはるかに優れており、保守が簡単です。

_int retval = 1;
tmp_string = strdup(string);
token = strtok(tmp_string,delim);
while( token != NULL ){
    ...
    some statements, some involving dynamically allocated memory
    ...
    if ( check_this() ){
        retval = 1;
        goto out_free;
    }
    ...
    some more stuff
    ...
    if(something()){
        if ( check_that() ){
            retval = 1;
            goto out_free;
        } else {
            retval = 0;
            goto out_free;
        }
    }
    token = strtok(NULL,delim);
}

out_free:
free(var1);
free(var2);
...
free(varN);
return retval;
_

さて、コードには他のあらゆる種類の問題がありました。つまり、Nが10を超えていて、関数が450行を超えていて、場所によっては10レベルの入れ子があったということです。

しかし、私は上司にそれをリファクタリングするように頼みました、私はそれをしました、そして今それはすべて短い機能の束です、そしてそれらはすべてLinuxスタイルを持っています

_int function(const char * param)
{
    int retval = 1;
    char * var1 = fcn_that_returns_dynamically_allocated_string(param);
    if( var1 == NULL ){
        retval = 0;
        goto out;
    }

    if( isValid(var1) ){
         retval = some_function(var1);
         goto out_free;
    }

    if( isGood(var1) ){
         retval = 0;
         goto out_free;
    }

out_free:
    free(var1);
out:
    return retval;
}
_

gotosなしの同等のものを検討する場合:

_int function(const char * param)
{
    int retval = 1;
    char * var1 = fcn_that_returns_dynamically_allocated_string(param);
    if( var1 != NULL ){

       if( isValid(var1) ){
            retval = some_function(var1);
       } else {
          if( isGood(var1) ){
               retval = 0;
          }
       }
       free(var1);

    } else {
       retval = 0;
    }

    return retval;
}
_

最初のケースでは、最初の関数がNULLを返した場合、ここから外れて_0_が返されることは明らかです。 2番目のケースでは、ifに関数全体が含まれていることを確認するために、下にスクロールする必要があります。最初の1つは私にこれを文体的に示し(名前 "out")、2つ目は構文的にそれを行います。最初のものはさらに明白です。

また、関数の最後にfree()ステートメントを置くことを強くお勧めします。これは、私の経験では、関数の途中のfree()ステートメントが悪臭を放ち、サブルーチンを作成する必要があることを示しているためです。この場合、関数で_var1_を作成しましたが、サブルーチンでfree()できませんでしたが、それが_goto out_free_、goto outスタイルが実用的である理由です。

プログラマーはgotoが悪だと信じて育てられる必要があると思います。次に、十分に成熟したら、Linuxソースコードを参照してLinuxスタイルガイドを読む必要があります。

このスタイルを非常に一貫して使用していることを追加する必要があります。すべての関数にはint retval、_out_free_ラベル、およびoutラベルがあります。文体の一貫性により、読みやすさが向上しています。

ボーナス:中断して続行します

あなたがwhileループを持っているとしましょう

_char *var1, *var2;
char line[MAX_LINE_LENGTH];
while( sscanf(line,... ){
    var1 = functionA(line,count);
    var2 = functionB(line,count);

    if( functionC(var1, var2){
         count++
         continue;
    }

    ...
    a bunch of statements
    ...

    count++;
    free(var1);
    free(var2);
}
_

このコードには他にも問題がありますが、1つはcontinueステートメントです。全体を書き直したいのですが、少し変更する必要がありました。満足できる方法でリファクタリングするのに数日かかったでしょうが、実際の変更は約半日の作業でした。問題は、「continue」を実行しても、_var1_および_var2_を解放する必要があることです。 _var3_を追加する必要があり、free()ステートメントをミラー化する必要があることを指摘しました。

当時、私は比較的新しいインターンでしたが、しばらくの間Linuxのソースコードを楽しんでいたので、上司にgotoステートメントを使用できるかどうか尋ねました。彼はそう言った、そして私はこれをやった:

_char *var1, *var2;
char line[MAX_LINE_LENGTH];
while( sscanf(line,... ){
    var1 = functionA(line,count);
    var2 = functionB(line,count);
    var3 = newFunction(line,count);

    if( functionC(var1, var2){
         goto next;
    }

    ...
    a bunch of statements
    ...
next:
    count++;
    free(var1);
    free(var2);
}
_

続けても大丈夫だと思いますが、私にとってはラベルが見えない後藤のようです。休憩も同じです。この場合のように、複数の場所で変更をミラーリングすることが強制されない限り、私は続行または中断を優先します。

また、_goto next;_と_next:_ラベルのこの使用は私には満足できないことも付け加えておきます。これらは、free()および_count++_ステートメントをミラーリングするよりも優れています。

gotoはほとんどの場合間違っていますが、いつ使用するのが適切かを知る必要があります。

私が議論しなかったことの1つは、他の回答でカバーされているエラー処理です。

パフォーマンス

Strtok()の実装を見ることができます http://opensource.Apple.com//source/Libc/Libc-167/string.subproj/strtok.c

_#include <stddef.h>
#include <string.h>

char *
strtok(s, delim)
    register char *s;
    register const char *delim;
{
    register char *spanp;
    register int c, sc;
    char *tok;
    static char *last;


    if (s == NULL && (s = last) == NULL)
        return (NULL);

    /*
     * Skip (span) leading delimiters (s += strspn(s, delim), sort of).
     */
cont:
    c = *s++;
    for (spanp = (char *)delim; (sc = *spanp++) != 0;) {
        if (c == sc)
            goto cont;
    }

    if (c == 0) {       /* no non-delimiter characters */
        last = NULL;
        return (NULL);
    }
    tok = s - 1;

    /*
     * Scan token (scan for delimiters: s += strcspn(s, delim), sort of).
     * Note that delim must have one NUL; we stop if we see that, too.
     */
    for (;;) {
        c = *s++;
        spanp = (char *)delim;
        do {
            if ((sc = *spanp++) == c) {
                if (c == 0)
                    s = NULL;
                else
                    s[-1] = 0;
                last = s;
                return (tok);
            }
        } while (sc != 0);
    }
    /* NOTREACHED */
}
_

私が間違っている場合は修正してください。ただし、パフォーマンスのために_cont:_ラベルと_goto cont;_ステートメントがあると思います(コードが読みやすくなることはありません)。それらは、

_while( isDelim(*s++,delim));
_

区切り文字をスキップします。しかし、可能な限り速く、不必要な関数呼び出しを避けるために、彼らはこのようにしています。

ダイクストラの論文を読んだところ、かなり難解だと思いました。

google "dijkstra gotoステートメントは有害だと見なされました"私は2つ以上のリンクを投稿する評判がないためです。

Gotoを使用しない理由として引用されているのを見たことがありますが、私のgotoの使用が認められている限り、それを読んでも何も変わりません。

補遺

継続と中断についてこのすべてを考えながら、私はきちんとした規則を思いつきました。

  • Whileループ内に継続がある場合、whileループの本体は関数であり、continueはreturnステートメントでなければなりません。
  • Whileループ内にbreakステートメントがある場合、whileループ自体が関数であり、breakがreturnステートメントになるはずです。
  • 両方ある場合は、何かが間違っている可能性があります。

スコープの問題が原因で常に可能であるとは限りませんが、これを行うと、コードについて推論するのがはるかに簡単になることがわかりました。私は、whileループが中断または継続しているときはいつでも、悪い感じを与えることに気づきました。

7

個人的には、次のようにリファクタリングします。

int DoLotsOfStuffThatCouldFail (paramstruct *params)
{
    int errcode = EC_NOERROR;

    if ((errcode = FunctionThatCouldFail1 (params)) != EC_NOERROR) return errcode;
    if ((errcode = FunctionThatCouldFail2 (params)) != EC_NOERROR) return errcode;
    if ((errcode = FunctionThatCouldFail3 (params)) != EC_NOERROR) return errcode;
    if ((errcode = FunctionThatCouldFail4 (params)) != EC_NOERROR) return errcode;

    return EC_NOERROR;
}

void DoStuff (paramstruct *params)
{
    int errcode = EC_NOERROR;

    InitStuffThatMayNeedToBeCleaned (params);

    if ((errcode = DoLotsOfStuffThatCouldFail (params)) != EC_NOERROR)
    {
         CleanupAfterError (params, errcode);
    }
}

ただし、gotoを回避するよりも深い入れ子を回避する方がモチベーションが高く(IMOは最初のコードサンプルの問題が悪化します)、当然のことながらCleanupAfterErrorがスコープ外で発生する可能性があります(この場合、 "params"は解放する必要がある割り当てられたメモリ、閉じる必要があるFILE *などを含む構造体です)。

このアプローチで私が目にする主な利点の1つは、たとえばFTCF2とFTCF3の間に架空の将来の追加ステップを配置する(または既存の現在のステップを削除する)のが簡単かつクリーンであるため、保守性(および私をリンチしたくない私のコードを継承します!)-さておき、ネストされたバージョンにはそれがありません。

5
Maximus Minimus

厳格な基準(例に適合する)でgotoを許可するMISRA(モーター産業ソフトウェア信頼性協会)Cコーディングガイドラインをご覧ください。

私が作業する場所では、同じコードが記述されます-gotoは必要ありません-それらについての不必要な宗教的な議論を避けることは、どのソフトウェアハウスでも大きなプラスです。

error = function_that_could_fail_1();
if(!error) {
  error = function_that_could_fail_2();
}
if(!error) {
  error = function_that_could_fail_3();
} 
if(!error) {
...
if (error) {
  cleanup:
} 

または「goto in drag」の場合-gotoよりも危険なものですが、「No goto Ever !!!」を回避します。キャンプ)「確かに大丈夫、後藤は使わない」....

do {
  if (error = function_that_could_fail_1() ){
    break 
  }
  if (error = function_that_could_fail_2() ){
    break 
  }
  ....... 
} while (0) 
cleanup();
.... 

関数が同じパラメーター型を持っている場合は、それらをテーブルに入れてループを使用します-

3
mattnz

代わりの場合はgotoも使用しますdo/while/continue/break hackeryはless読み取り可能です。

gotosには、ターゲットに名前があり、それらがgoto something;。これは、実際に何かを停止または継続していない場合は、breakまたはcontinueより読みやすいかもしれません。

1
aib