web-dev-qa-db-ja.com

gotoの使用を回避し、ネストされたループを効率的に中断する方法

gotoを使用することは、C/C++でのプログラミングに関しては悪い習慣と見なされているという事実だと思います。

ただし、次のコードが与えられた場合

for (i = 0; i < N; ++i) 
{
    for (j = 0; j < N; j++) 
    {
        for (k = 0; k < N; ++k) 
        {
            ...
            if (condition)
                goto out;
            ...
        }
    }
}
out:
    ...

gotoを使用せずに同じ動作を効率的に達成する方法を疑問に思います。つまり、たとえば、すべてのループの最後でconditionをチェックするなどのことができますが、AFAIK gotoはjmpになるAssembly命令を1つだけ生成します。だから、これは私が考えることができる最も効率的な方法です。

良い習慣と考えられる他のものはありますか? gotoを使用するのは悪い習慣と見なされると言ったのは間違っていますか?もしそうなら、これはそれを使うのが良いケースの一つでしょうか?

ありがとうございました

30
rual93

(imo)最高の非gotoバージョンは次のようになります。

void calculateStuff()
{
    // Please use better names than this.
    doSomeStuff();
    doLoopyStuff();
    doMoreStuff();
}

void doLoopyStuff()
{
    for (i = 0; i < N; ++i) 
    {
        for (j = 0; j < N; j++) 
        {
            for (k = 0; k < N; ++k) 
            {
                /* do something */

                if (/*condition*/)
                    return; // Intuitive control flow without goto

                /* do something */
            }
        }
    }
}

これを分割することも、関数を短くし、コードを読みやすくする(関数よりも名前を付けた方がいい場合)および依存関係を低く抑えるのに役立つため、おそらく良い考えです。

47
Max Langhof

Ifそのような深くネストされたループがあり、あなたmustが抜け出す必要がある場合、gotoは最高のソリューション。一部の言語(Cではない)には、複数のループから抜け出すbreak(N)ステートメントがあります。 Cにないのは、gotoよりもさらに悪いworseであるためです:ネストループをカウントして把握する必要がありますブレークカウントを調整する必要があることに気付かずに、後でやって来てネストのレベルを追加または削除する誰かに対して脆弱です。

はい、gotosは一般的に眉をひそめています。ここでgotoを使用するのは良い解決策ではありません。それは単にいくつかの悪の中で最も小さいものです。

ほとんどの場合、深くネストされたループから抜け出す必要があるのは、何かを探していて、それを見つけたからです。その場合(および他のいくつかのコメントと回答が示唆しているように)、ネストされたループを独自の関数に移動することを好みます。その場合、内部ループのreturnがタスクを非常にきれいに完了します。

(関数は常に中央からではなく最後に戻らなければならないと言う人がいます。そのため、関数への簡単なブレークアウトソリューションは無効であり、使用を強制します。検索が独自の機能に分割された場合でも、同じ厄介な内部ループからの脱出テクニックです。個人的には、それらの人々は間違っていると思いますが、あなたのマイレージは異なる場合があります。

gotoを使用しないことを主張し、早期復帰の別の関数を使用しないことを主張する場合、はい、追加のブール制御変数を維持し、制御条件で冗長にテストするなどのことができます入れ子になった各ループの、しかしそれは単に迷惑であり混乱です。 (単純なgotoを使用することはそれよりも小さいと言っていた、それはより大きな悪の1つです。)

24
Steve Summit

ここでgotoは完全に正しいことであり、 C++コアガイドライン による例外的なユースケースの1つであると思います。

ただし、おそらく考慮すべき別のソリューションは、 [〜#〜] iife [〜#〜] ラムダです。私の意見では、これは別の関数を宣言するよりもわずかにエレガントです!

[&] {
    for (int i = 0; i < N; ++i)
        for (int j = 0; j < N; j++)
            for (int k = 0; k < N; ++k)
                if (condition)
                    return;
}();

この提案に対するredditの JohnMcPineapple に感謝します!

15
ricco19

この場合、あなたはdo n'tgotoの使用を避ける必要はありません。

一般的にgotoの使用は避けるべきですが、このルールには例外があり、あなたのケースはそれらの良い例です。

代替案を見てみましょう。

for (i = 0; i < N; ++i) {
    for (j = 0; j < N; j++) {
        for (k = 0; k < N; ++k) {
            ...
            if (condition)
                break;
            ...
        }
        if (condition)
            break;
    }
    if (condition)
        break;
}

または:

int flag = 0
for (i = 0; (i < N) && !flag; ++i) {
    for (j = 0; (j < N) && !flag; j++) {
        for (k = 0; (k < N) && !flag; ++k) {
            ...
            if (condition) {
                flag = 1
                break;
            ...
        }
    }
}

これらのどちらも、gotoバージョンほど簡潔でも読みやすいものでもありません。

gotoを使用することは、前方にジャンプするだけで(後方ではなく)、コードを読みやすく理解しやすくする場合に受け入れられると見なされます。

一方、gotoを使用して両方向にジャンプする場合、または変数の初期化をバイパスする可能性のあるスコープにジャンプする場合、thatは悪いでしょう。

gotoの悪い例は次のとおりです。

int x;
scanf("%d", &x);
if (x==4) goto bad_jump;

{
    int y=9;

// jumping here skips the initialization of y
bad_jump:

    printf("y=%d\n", y);
}

gotoyの初期化をジャンプするため、C++コンパイラはここでエラーをスローします。ただし、Cコンパイラはこれをコンパイルし、上記のコードは、yが発生した場合に初期化されないgotoを出力しようとするときに ndefined behavior を呼び出します。

gotoの適切な使用の別の例は、エラー処理です。

void f()
{
    char *p1 = malloc(10);
    if (!p1) {
        goto end1;
    }
    char *p2 = malloc(10);
    if (!p2) {
        goto end2;
    }
    char *p3 = malloc(10);
    if (!p3) {
        goto end3;
    }
    // do something with p1, p2, and p3

end3:
    free(p3);
end2:
    free(p2);
end1:
    free(p1);
}

これにより、関数の最後にすべてのクリーンアップが実行されます。これを代替案と比較してください:

void f()
{
    char *p1 = malloc(10);
    if (!p1) {
        return;
    }
    char *p2 = malloc(10);
    if (!p2) {
        free(p1);
        return;
    }
    char *p3 = malloc(10);
    if (!p3) {
        free(p2);
        free(p1);
        return;
    }
    // do something with p1, p2, and p3

    free(p3);
    free(p2);
    free(p1);
}

クリーンアップが複数の場所で行われる場所。後でクリーンアップする必要のあるリソースをさらに追加する場合は、これらすべての場所にクリーンアップを追加し、以前に取得したリソースのクリーンアップを追加することを忘れないでください。

上記の例はC++よりもCに関連しています。後者の場合、適切なデストラクタとスマートポインタを持つクラスを使用して、手動でのクリーンアップを回避できるためです。

12
dbush

ラムダを使用すると、ローカルスコープを作成できます。

[&]{
  for (i = 0; i < N; ++i) 
  {
    for (j = 0; j < N; j++) 
    {
      for (k = 0; k < N; ++k) 
      {
        ...
        if (condition)
          return;
        ...
      }
    }
  }
}();

returnをその範囲外にする機能も必要な場合:

if (auto r = [&]()->boost::optional<RetType>{
  for (i = 0; i < N; ++i) 
  {
    for (j = 0; j < N; j++) 
    {
      for (k = 0; k < N; ++k) 
      {
        ...
        if (condition)
          return {};
        ...
      }
    }
  }
}()) {
  return *r;
}

{}またはboost::nulloptを返すことは「ブレーク」であり、値を返すと、囲んでいるスコープから値が返されます。

別のアプローチは次のとおりです。

for( auto idx : cube( {0,N}, {0,N}, {0,N} ) {
  auto i = std::get<0>(idx);
  auto j = std::get<1>(idx);
  auto k = std::get<2>(idx);
}

ここで、3つの次元すべてにわたって反復可能オブジェクトを生成し、深さ1のネストされたループにします。 breakは正常に動作するようになりました。 cubeと書く必要があります。

c ++ 17 では、これは

for( auto[i,j,k] : cube( {0,N}, {0,N}, {0,N} ) ) {
}

いいね.

さて、あなたが反応するはずのアプリケーションでは、一次制御フローレベルで大きな3次元範囲をループするのはよくない考えです。スレッド化することもできますが、それでもスレッドの実行時間が長すぎるという問題が発生します。そして、私がプレイしたほとんどの3次元の大規模なイテレーションは、サブタスクスレッド自体を使用することで恩恵を受けることができます。

そのためには、アクセスするデータの種類に基づいて操作を分類し、繰り返しをスケジュールする何かに操作を渡しますfor yo

auto work = do_per_Voxel( volume,
  [&]( auto&& Voxel ) {
    // do work on the Voxel
    if (condition)
      return Worker::abort;
    else
      return Worker::success;
  }
);

その後、関連する制御フローはdo_per_Voxel関数に入ります。

do_per_Voxelは単純な裸のループではなく、ボクセルごとのタスクをスキャンラインごとのタスクに書き換えるシステム(または、実行時のプレーン/スキャンラインの大きさに応じてプレーンごとのタスクでさえも) (!))次に、それらをスレッドプール管理タスクスケジューラに順番にディスパッチし、結果のタスクハンドルをつなぎ合わせて、作業を待つときまたは継続トリガーとして使用できる将来のようなworkを返します。終わらせる。

また、gotoを使用することもあります。または、サブループの関数を手動で分割します。または、フラグを使用して、深い再帰から抜け出します。または、3層のループ全体を独自の関数に配置します。または、モナドライブラリを使用してループ演算子を作成します。または、例外(!)をスローしてキャッチできます。

c ++ のほとんどeveryの質問への答えは「依存する」です。問題の範囲と利用可能なテクニックの数は多く、問題の詳細によってソリューションの詳細が変わります。

代替-1

次のようなことができます:

  1. 先頭にbool変数を設定しますisOkay = true
  2. すべてのforloopconditions、追加の条件isOkay == true
  3. カスタム条件が満たされた/失敗したら、isOkay = false

これにより、ループが停止します。ただし、余分なbool変数が便利な場合があります。

bool isOkay = true;
for (int i = 0; isOkay && i < N; ++i)
{
    for (int j = 0; isOkay && j < N; j++)
    {
        for (int k = 0; isOkay && k < N; ++k)
        {
            // some code
            if (/*your condition*/)
                 isOkay = false;
        }
     }
}

代替-2

第二に。上記のループの繰り返しが関数内にある場合、最良の選択は、カスタム条件が満たされたときにreturn resultにすることです。

bool loop_fun(/* pass the array and other arguments */)
{
    for (int i = 0; i < N ; ++i)
    {
        for (int j = 0; j < N ; j++)
        {
            for (int k = 0; k < N ; ++k)
            {
                // some code
                if (/* your condition*/)
                    return false;
            }
        }
    }
    return true;
}
4
JeJo

Forループを関数に分割します。これにより、各ループが実際に何をしているのかを見ることができるため、物事を非常に理解しやすくなります。

bool doHerpDerp() {
    for (i = 0; i < N; ++i) 
    {
        if (!doDerp())
            return false;
    }
    return true;
}

bool doDerp() {
    for (int i=0; i<X; ++i) {
        if (!doHerp())
            return false;
    }
    return true;
}

bool doHerp() {
    if (shouldSkip)
        return false;
    return true;
}
3
UKMonkey

良い習慣と考えられる他のものはありますか? gotoを使用するのは悪い習慣と見なされると言ったのは間違っていますか?

gotoは誤って使用されたり使い古されたりする可能性がありますが、この例にはこの2つはありません。深くネストされたループから抜け出すことは、単純なgoto label_out_of_the_loop;

異なるラベルにジャンプする多くのgotosを使用することは悪い習慣ですが、そのような場合、キーワードgoto自体がコードを悪くすることはありません。あなたがコードを飛び回っていて、それを追うのが難しくなっているのは悪いことです。ただし、ネストされたループから1回ジャンプする必要がある場合は、まさにその目的のために作成されたツールを使用しないでください。

薄い空気の類似性から作られたものを使用するには:過去に壁に釘を打ち込むことが流行だった世界に住んでいると想像してください。最近では、ドライバーを使用して壁にネジを開けることがよりファッショナブルになり、ハンマーは完全に時代遅れです。今、あなたは(少し古臭いにも関わらず)壁に釘を入れなければならないことを考慮してください。あなたはそれを行うためにハンマーを使用することを控えるべきではありませんが、おそらくあなたが本当にネジではなく壁に釘が必要かどうかを自問するべきです。

(はっきりしない場合は:ハンマーはgotoで、壁の釘は入れ子になったループから飛び出しますが、壁のネジは関数を使用して深い入れ子を回避します;)

特定

IMO、この特定の例では、ループ間の共通機能に注意することが重要だと思います。 (ここであなたの例が必ずしもリテラルであるとは限らないことを知っていますが、ちょっとだけ耐えてください)各ループがN回繰り返されるので、次のようにコードを再構築できます:

_int max_iterations = N * N * N;
for (int i = 0; i < max_iterations; i++)
{
    /* do stuff, like the following for example */
    *(some_ptr + i) = 0; // as opposed to *(some_3D_ptr + i*X + j*Y + Z) = 0;
    // some_arr[i] = 0; // as oppose to some_3D_arr[i][j][k] = 0;
}
_

ここで、forまたはそれ以外のループはすべて、if-gotoパラダイムの単なる合成シュガーであることを覚えておくことが重要です。関数が結果を返すようにする必要があることを他の人に同意しますが、上記のような場合には当てはまらない例を示したいと思います。確かに、コードレビューで上記にフラグを立てますが、上記をgotoに置き換えた場合、間違った方向に進んだと考えます。 (注-希望するデータ型に確実に適合できることを確認してください)

全般

さて、一般的な答えとして、ループの終了条件は毎回同じではないかもしれません(問題の投稿のように)。原則として、できるだけ多くの不要な操作(乗算など)をループから引き出します。コンパイラーは日々賢くなっていますが、効率的で読みやすいコードを作成するのに代わるものはありません。

_/* matrix_length: int of m*n (row-major order) */
int num_squared = num * num;
for (int i = 0; i < matrix_length; i++)
{
    some_matrix[i] *= num_squared; // some_matrix is a pointer to an array of ints of size matrix_length
}
_

_*= num * num_と書くのではなく、これを最適化するためにコンパイラーに依存する必要はなくなりました(ただし、優れたコンパイラーはそうすべきです)。したがって、上記の機能を実行する2重または3重にネストされたループは、コードだけでなく、IMOがクリーンで効率的なコードを作成するのにも役立ちます。最初の例では、代わりに*(some_3D_ptr + i*X + j*Y + Z) = 0;を使用できます。コンパイラが_i*X_および_j*Y_を最適化して、繰り返しごとに計算されないようにすることを信頼していますか?

_bool check_threshold(int *some_matrix, int max_value)
{
    for (int i = 0; i < rows; i++)
    {
        int i_row = i*cols; // no longer computed inside j loop unnecessarily.
        for (int j = 0; j < cols; j++)
        {
            if (some_matrix[i_row + j] > max_value) return true;
        }
    }
    return false;
}
_

うん! STLまたはBoostのようなライブラリが提供するクラスを使用しないのはなぜですか? (ここで低レベル/高パフォーマンスのコードを実行する必要があります)。複雑なため、3Dバージョンを作成することさえできませんでした。手で最適化したものがありますが、コンパイラで許可されている場合は、#pragma unrollまたは同様のプリプロセッサヒントを使用することをお勧めします。

結論

一般的に、使用できる抽象化レベルが高いほど優れていますが、たとえば、1次元の行の主要な整数行列を2次元の配列にエイリアスすると、コードフローの理解/拡張が難しくなります。 ?同様に、それは何かを独自の機能にするための指標にもなります。これらの例を考えると、さまざまな場所でさまざまなパラダイムが求められていること、そしてそれを理解するプログラマとしてのあなたの仕事がわかることを願っています。上記に夢中になってはいけませんが、それらが何を意味するのか、それらをどのように使用するのか、いつ呼び出されるのか、そして最も重要なことは、コードベースを使用する他の人々が彼らが何であるかを知っていることを確認してくださいそれについて何の心配もありません。がんばろう!

2
jfh

visual Studio 2017のリリースモードで両方のオプションをコンパイルする効率に関するあなたのコメントに関しては、まったく同じアセンブリが生成されます。

for (int i = 0; i < 5; ++i)
{
    for (int j = 0; j < 5; ++j)
    {
        for (int k = 0; k < 5; ++k)
        {
            if (i == 1 && j == 2 && k == 3) {
                goto end;

            }
        }
    }
}
end:;

フラグ付き。

bool done = false;
for (int i = 0; i < 5; ++i)
{
    for (int j = 0; j < 5; ++j)
    {
        for (int k = 0; k < 5; ++k)
        {
            if (i == 1 && j == 2 && k == 3) {
                done = true;
                break;
            }
        }
        if (done) break;
    }
    if (done) break;
}

どちらも生産します。

xor         edx,edx  
xor         ecx,ecx  
xor         eax,eax  
cmp         edx,1  
jne         main+15h (0C11015h)  
cmp         ecx,2  
jne         main+15h (0C11015h)  
cmp         eax,3  
je          main+27h (0C11027h)  
inc         eax  
cmp         eax,5  
jl          main+6h (0C11006h)  
inc         ecx  
cmp         ecx,5  
jl          main+4h (0C11004h)  
inc         edx  
cmp         edx,5  
jl          main+2h (0C11002h)    

ゲインはありません。最新のc ++コンパイラを使用する場合の別のオプションは、ラムダでラップすることです。

[](){
for (int i = 0; i < 5; ++i)
{
    for (int j = 0; j < 5; ++j)
    {
        for (int k = 0; k < 5; ++k)
        {
            if (i == 1 && j == 2 && k == 3) {
                return;
            }
        }

    }
}
}();

これもまったく同じアセンブリを生成します。個人的には、例でgotoを使用することは完全に受け入れられると思います。他の誰かに何が起こっているかは明らかであり、より簡潔なコードを作成します。ほぼ間違いなく、ラムダも同様に簡潔です。

2
rmawatson

コードをリファクタリングする方法を説明する優れた回答が既にいくつかありますので、繰り返しません。効率のためにそのようにコーディングする必要はもうありません。問題は、それがエレガントかどうかです。 (さて、私が提案する改良点の1つ:ヘルパー関数がその1つの関数の本体内でのみ使用されることを意図している場合、オプティマイザーがstaticを宣言することにより、オプティマイザーを助けるかもしれません。この関数は外部リンケージを持たず、他のモジュールから呼び出されることはなく、ヒントinlineを傷つけることはできませんが、ラムダを使用する場合、最新のコンパイラには何も必要ありませんそのようなヒント。)

質問の構成に少し挑戦します。あなたは、ほとんどのプログラマーがgotoを使用することに対してタブーを持っていることは正しいです。私の意見では、これは元の目的を見失っています。 Edsger Dijkstraが「Go To Statement考慮された有害」と書いたとき、彼がそう考えた特定の理由がありました:go toの「自由な」使用は、現在のプログラム状態について正式に推論するのが非常に難しく、どのような条件が必要か現時点では、再帰関数呼び出し(彼が好んだ)または反復ループ(彼が受け入れた)からの制御フローと比較して真です。彼は結論付けた:

go toステートメントはそのままではあまりにも原始的です。自分のプログラムを台無しにするのはあまりにも誘いです。その使用をつなぐとみなされる条項を考慮し、評価することができます。言及された節はすべてのニーズを満たすという意味で網羅的であるとは主張しませんが、提案された節(中絶節など)は、プログラマーに依存しない座標系を維持してプロセスを記述することができるという要件を満たす必要があります便利で管理しやすい方法。

RustとJavaなど)のようなC言語のようなプログラミング言語の多くには、ラベルにbreakという追加の「節を使用すると考えられる節」があります。ネストされたループの2つのレベルから抜け出し、それらを含むループの先頭から再開するbreak 2 continue;のようなものかもしれません。これは、Cスタイルのbreakがダイクストラに対してプログラマーが頭の中で追跡できるプログラムの状態の簡潔な説明を定義するか、静的アナライザーが扱いやすいと判断した場合。

gotoをこのような構造に制限すると、ラベルの名前が変更されただけの区切りになります。それに関する残りの問題は、コンパイラとプログラマが必ずしもあなたがそれをこのように使用することを知っているとは限らないことです。

ループの後に保持される重要な事後条件があり、gotoに対する懸念がダイクストラのものと同じである場合は、// We have done foo to every element, or encountered condition and stopped.のような短いコメントでそれを述べることを検討するかもしれません。人間の場合、静的アナライザーで問題ないはずです。

1
Davislor

可能な方法の1つは、状態を表す変数にブール値を割り当てることです。この状態は、後でコードの他の目的で「IF」条件ステートメントを使用してテストできます。

1
Chigozie Orunta
bool meetCondition = false;
for (i = 0; i < N && !meetCondition; ++i) 
{
    for (j = 0; j < N && !meetCondition; j++) 
    {
        for (k = 0; k < N && !meetCondition; ++k) 
        {
            ...
            if (condition)
                meetCondition = true;
            ...
        }
    }
}
1
nameless1

最適な解決策は、関数にループを配置し、その関数からreturnを配置することです。

これは基本的にgotoの例と同じですが、大規模な利点を使用すると、別のgoto討論を避けることができます。

簡略化された擬似コード:

bool function (void)
{
  bool result = something;


  for (i = 0; i < N; ++i) 
    for (j = 0; j < N; j++) 
      for (k = 0; k < N; ++k) 
        if (condition)
          return something_else;
  ...
  return result;
}

ここでのもう1つの利点は、3つ以上のシナリオに遭遇した場合にboolからenumにアップグレードできることです。 gotoで読みやすい方法でそれを行うことはできません。複数のgotoと複数のラベルを使用し始めた瞬間は、スパゲッティコーディングを採用した瞬間です。はい、下方向に分岐しただけでも-読んで維持するのは面倒ではありません。

特に、forループが3つネストされている場合、コードをいくつかの関数に分割する必要があることを示している可能性があり、この議論全体は関連性がないかもしれません。

0
Lundin