web-dev-qa-db-ja.com

そのループで呼び出される関数内からループを抜け出す

現在、そのループで呼び出される関数内からforループから抜け出す方法を見つけようとしています。関数に値を返すだけで特定の値をチェックしてからブレークする可能性があることは承知していますが、関数内から直接実行したいと思います。これは、特定のハードウェア用に社内ライブラリを使用しているため、私の関数の関数シグネチャを次のようにする必要があるためです。

_void foo (int passV, int aVal, long bVal)
_

戻り値を使用しないことは非常に悪い習慣ですが、残念なことに私に強制されるので、ご容赦ください。

次の例を検討してください。

_#include <stdio.h>

void foo (int a) {
    printf("a: %d", a);
    break;
}

int main(void) {
    for (int i = 0; i <= 100; i++) {
        foo(i);
    }
    return 0;
}
_

現在、これはコンパイルされません。代わりに、次のようなコンパイルエラーが発生します。

prog.c:関数 'foo'内:prog.c:6:2:エラー:breakステートメントがループまたはswitch break内にありません。

私はこれが何を意味するか知っています(コンパイラはfoo()のブレークはforループ内にないと言います)

さて、breakステートメントに関する標準から見つけることができるのはこれです:

Breakステートメントは、while、do、for、またはswitchステートメントを含む最も内側の次のステートメントに制御を渡します。構文は単純です。

私の関数がforループ内から呼び出されることを考えると、なぜbreakステートメントはforループから抜け出ないのですか?さらに、関数を最初に返すことなく、このようなことを実現することは可能ですか?

29
Magisch

breakは、gotoと同じように、同じ関数内でのみローカルにジャンプできますが、どうしても必要な場合は、setjmplongjmpを使用できます。

#include <stdio.h>
#include <setjmp.h>

jmp_buf jump_target;

void foo(void)
{
    printf("Inside foo!\n");
    longjmp(jump_target, 1);
    printf("Still inside foo!\n");
}

int main(void) {
    if (setjmp(jump_target) == 0)
        foo();
    else
        printf("Jumped out!\n");
    return 0;
}

longjmpを呼び出すと、setjmp呼び出しに戻ります。 setjmpからの戻り値は、ジャンプターゲットを設定した後に戻るか、ジャンプから戻るかを示します。

出力:

Inside foo!
Jumped out!

非ローカルジャンプは正しく使用すれば安全ですが、次の点について慎重に検討する必要があります。

  • longjmpsetjmp呼び出しとlongjmp呼び出しの間のすべての関数アクティベーションを「ジャンプ」するので、これらの関数のいずれかが現在の後に追加の作業を行えると予想される場合実行に移すと、その作業は単に行われません。
  • setjmpを呼び出した関数のアクティブ化が終了した場合、動作は未定義です。何でも起れる。
  • setjmpがまだ呼び出されていない場合、jump_targetは設定されておらず、動作は未定義です。
  • setjmpを呼び出した関数内のローカル変数は、特定の条件下で未定義の値を持つことができます。
  • 一言:スレッド。
  • 浮動小数点ステータスフラグが保持されない可能性があり、setjmp呼び出しを配置できる場所に制限があるなど、その他のこともあります。

マシン命令とCPUレジスタのレベルで非ローカルジャンプが何をするかを十分に理解している場合、これらのほとんどは自然に続きますが、それがない限り、andはC標準の動作と動作を読んでいます。保証するものではありませんが、注意が必要です。

このように_break;_を使用することはできません。forループの本体内に表示する必要があります。

これを行う方法はいくつかありますが、どちらも推奨されません。

  • exit()関数を使用してプログラムを終了できます。ループはmain()から実行され、その後は何もしないので、この方法で目的を達成することは可能ですが、それは特別な場合です。

  • 関数にグローバル変数を設定し、関数呼び出し後のforループでテストできます。通常、グローバル変数の使用は推奨されません。

  • setjmp()longjmp()を使用できますが、ハンマーでハエを押しつぶそうとするようなもので、他のものを壊して完全にハエを逃す可能性があります。このアプローチはお勧めしません。さらに、関数に渡すかグローバル変数としてアクセスする必要があるjmpbufが必要です。

許容可能な代替方法は、status変数のアドレスを追加の引数として渡すことです。関数は、ループから抜ける必要があることを示すためにそれを設定できます。

ただし、Cでの最善のアプローチは、継続をテストするための値を返すことです、最も読みやすいです。

あなたの説明から、foo()のソースコードはありませんが、foo()によって直接または間接的に呼び出される変更可能な関数のいくつかの条件を検出できます:longjmp()は、foo()の内部の深い位置から、おそらくスタックの多くのレベルでsetjmp()位置にジャンプし、すべての中間呼び出しの通常の関数終了コードをバイパスします。クラッシュを回避するために正確にそれが必要な場合は、setjmp()/longjmp()が解決策ですが、リソースのリーク、初期化の欠落、一貫性のない状態などの問題を引き起こす可能性があります未定義の動作の原因。

_101_演算子を使用しているため、forループは_<=_回反復することに注意してください。慣用的なforループは、for (int i = 0; i < 100; i++)を使用して、上限(除外)として現れる回数を正確に反復します。

44
chqrlie

(注:質問は最初にこれを書いてから編集されています)

Cのコンパイル方法のため、関数が呼び出されたときにどこにブレークするかを知る必要があります。どこからでも、あるいはブレークが意味をなさないどこからでも呼び出すことができるため、関数に_break;_ステートメントを入れて、このように機能させることはできません。

他の回答では、グローバル変数の設定、関数からの_#define_またはlongjumping(!)の使用など、ひどい解決策が提案されています。これらは非常に貧弱なソリューションです。代わりに、最初の段落で誤って却下したソリューションを使用し、この場合breakをトリガーする状態を示す関数から値を返し、次のようなことを行う必要があります。

_#include <stdbool.h>

bool checkAndDisplay(int n)
{
    printf("%d\n", n);
    return (n == 14);
}

int main(void) {
    for (int i = 0; i <= 100; i++) {
        if (checkAndDisplay(i))
            break;
    }
    return 0;
}
_

正しい方法を使用して同じ最終結果を達成するのではなく、このようなことを達成するためのあいまいな方法を見つけようとすることは、保守とデバッグが困難である恐ろしい品質のコードを確実に生成する方法です。

コメントに隠されているが、voidリターンを使用する必要があると言いますが、これは問題ではなく、breakパラメーターをポインターとして渡すだけです。

_#include <stdbool.h>

void checkAndDisplay(int n, bool* wantBreak)
{
    printf("%d\n", n);
    if (n == 14)
        wantBreak = true;
}

int main(void) {
    bool wantBreak = false;
    for (int i = 0; i <= 100; i++) {
        checkAndDisplay(i, &wantBreak);
        if (wantBreak)
            break;
    }
    return 0;
}
_

パラメータは固定型なので、キャストを使用して、パラメータの1つへのポインタを渡すことをお勧めします。 foo(a, b, (long)&out);

9
Jack Aidley

breakは、コンパイル時に解決されるステートメントです。したがって、コンパイラは同じ関数内で適切なfor/whileループを見つける必要があります。他の場所から関数を呼び出せなかったという保証はないことに注意してください。

break命令を使用できない場合は、モジュールでローカル変数を定義し、2番目の実行条件をforループに追加できます。たとえば、次のコードのように:

#include <stdio.h>
#include <stdbool.h>

static bool continueLoop = true;

void foo (int a)
{
    bool doBreak = true;

    printf("a: %d",a);

    if(doBreak == true){
        continueLoop = false;
    }
    else {
        continueLoop = true;
    }
}
int main(void) {
    continueLoop = true;   // Has to be true before entering the loop
    for (int i = 0; (i <= 100) && continueLoop; i++)
    {
        foo(i);
    }
    return 0;
}

この例では、これは正確にbreak-命令ではありませんが、forloopは別の反復を行いません。 breakを実行する場合は、ifにつながる変数continueLoopを使用してbreak- conditionを挿入する必要があります。

int main(void) {
    continueLoop = true;   // Has to be true before entering the loop
    for (int i = 0; i <= 100; i++)
    {
        foo(i);
        if(!continueLoop){
            break;
        }
    }
    return 0;
}
6
Frodo

戻り値を処理できない場合は、少なくとも関数にパラメーターを追加できますか?そのような解決策を想像できます:

void main (void)
{
  int a = 0;

  for (; 1 != a;)
  {
    foo(x, &a);
  } 
}

void foo( int x, int * a)
{
  if (succeeded)
  {
    /* set the break condition*/
    *a = 1;
  }
  else
  {
    *a = 0;
  }
}

それは私の最初の投稿ですので、私のフォーマットが台無しになっている場合はご容赦ください:)

4

これは、実行可能または不可能な別のアイデアです。fooをno-opに変えることができる変数を保持します。

int broken = 0;

void foo (int a) {
    if (broken) return;

    printf("a: %d", a);
    broken = 1; // "break"
}

int main(void) {
    for (int i = 0; i <= 100; i++) {
        foo(i);
    }
    return 0;
}

これは機能的にはクロックサイクルの損失を除いて同じです(関数が呼び出されますが、ifステートメントのみを実行します)。ループを変更する必要はありません。これはスレッドセーフではなく、最初にしか機能しません(ただし、fooは、必要に応じてbrokenを0に設定して呼び出した場合、a変数を0にリセットできます)。

素晴らしいものではありませんが、まだ言及されていないアイデアです。

4
RemcoGerlich

このような場合は、forループの代わりに&&でチェーンされた複数の条件ステートメントでwhile()ループを使用することを検討してください。 can setjmpやlongjmpなどの関数を使用して通常の制御フローを変更しますが、どこでも悪い習慣と見なされています。理由を見つけるために、このサイトで一生懸命検索する必要はありません。 (要するに、デバッグや人間の理解に役立たない複雑な制御フローを作成する能力があるためです)

次のようなことも検討してください。

int foo (int a) {
    if(a == someCondition) return 0;
    else {
        printf("a: %d", a);
        return 1;
    }
}

int main(void) {
    for (int i = 0; i <= 100; i++) {
        if(!foo(i)) break;
    }
    return 0;
}

この場合、ループは 'foo'から返される真の値に依存します。'foo '内の条件が満たされない場合、ループは中断されます。

編集:goto、setjmp、longjmpなどの使用に明示的に反対しているわけではありません。しかし、この場合、これらの手段に頼らずに、はるかにシンプルで簡潔なソリューションが利用できると思います!

4
ajxs

breakステートメントがマシンコードに変換される方法に関連していると思います。 breakステートメントは、ループまたはスイッチの直後のラベルへの無条件分岐として変換されます。

_mov ECX,5
label1:
  jmp <to next instruction address>  ;break
loop label1
<next instruction>
_

ループ内からfoo()を呼び出すと、次のようになります

_mov ECX,5
label1:
  call <foo address>
loop label1
<next instruction>
_

およびfooアドレス

_call <printf address>
jmp <to where?> ;break cannot translate to any address.
_
4

更新された質問に明確に制限を設定した後、ループ全体を関数内に移動し、その関数内に戻り値を持つ2番目の関数を呼び出すことをお勧めします。

#include <stdbool.h>

bool foo (int x)
{
    return (x==14);
}

void loopFoo(int passV, int aVal, long bVal)
{
   for (int i = 0; i <= 100; ++i)
   {
       if (foo(x))
           break;
   }
}

これにより、制限を回避するための極端で壊れやすい体操が回避されます。

3
Jack Aidley

グローバル変数を設定し、ループでそれを確認するだけです:

#include <stdio.h>

int leave = 0;

void foo (int a) {
    printf("a: %d", a);
    leave = 1;
}

int main(void) {
    for (int i = 0; i <= 100; i++) {
        foo(i);
        if (leave)
          break;
    }
    return 0;
}
3
csd

forループで関数を手動でインライン化することを検討してください。この関数が複数のループで呼び出される場合、マクロとして定義します。

#define f()\
printf("a: %d", a);\
break;
2

ループ内の関数でエラーをスローし、ループ外でそのエラーをキャッチできます。

#include <stdio.h>

void foo (int a) {
    printf("a: %d", a);
    if (a == 50)
    {
       throw a;
    }
}

int main(void) {
    try {
        for (int i = 0; i <= 100; i++) {
            foo(i);
        }
    catch(int e) {
    }
    return 0;
}
2
Russell Hankins

この質問はすでに回答されていますが、c ++でループを終了するために可能なすべてのオプションを掘り下げる価値があると思います。基本的に5つの可能性があります。

  • ループ条件を使用する
  • break条件を使用する
  • return条件を使用する
  • 例外を使用する
  • gotoを使用する

以下では、c ++ 14を使用してこれらのオプションの使用例を説明します。ただし、C++の以前のバージョンではこれらすべてを実行できます(例外を除く)。短くするために、インクルードとメイン関数は省略します。いくつかの部分をより明確にする必要があると思われる場合は、コメントしてください。

1.ループ条件を使用する

ループを終了する標準的な方法は、ループ条件です。ループ条件は、forステートメントの中央部分、またはwhileステートメントの括弧の間に書き込まれます。

for(something; LOOP CONDITION; something) {
    ... 
}
while (LOOP CONDITION)
    ... 
}
do {
    ... 
} while (LOOP CONDITION);

ループ条件は、ループに入るかどうか、ループを繰り返すかどうかを決定します。上記のすべての場合、ループを繰り返すには、条件がtrueである必要があります。

例として、0〜2の数値を出力する場合、ループとループ条件を使用してコードを記述できます。

for (auto i = 0; i <= 2; ++i)
    std::cout << i << '\n';
std::cout << "done";

ここでは、条件はi <= 2。この条件がtrueと評価される限り、ループは実行を続けます。

別の実装では、代わりに条件を変数に入れます。

auto condition = false;

for (auto i = 0; !condition; ++i) {
    std::cout << i << '\n';
    condition = i > 2;
}
std::cout << "done";

両方のバージョンの出力を確認すると、目的の結果が得られます。

0
1
2
done

実際のアプリケーションでループ条件をどのように使用しますか?

どちらのバージョンも、c ++プロジェクト内で広く使用されています。最初のバージョンはよりコンパクトであるため、理解しやすいことに注意してください。ただし、条件がより複雑であるか、評価するためにいくつかのステップが必要な場合は、通常2番目のバージョンが使用されます。

例えば:

auto condition = false;
for (auto i = 0; !condition; ++i)
    if (is_prime(i))
        if (is_large_enough(i)) {
            key = calculate_cryptographic_key(i, data);
            if (is_good_cryptographic_key(key))
                condition = true;
        }

2. break条件を使用する

ループを終了するもう1つの簡単な方法は、breakキーワードを使用することです。ループ内で使用された場合、実行は停止し、ループ本体の後に続きます。

for (auto i = 0; true; ++i) {
    if (i == 3)
        break;
    std::cout << i << '\n';
}
std::cout << "done";

これにより、現在の数値が出力され、iの値が3に達するまで1ずつ増加します。ここで、ifステートメントはbreak条件です。条件がtrueの場合、ループが壊れています(!)次の行から実行が継続され、doneが出力されます。

テストを実行すると、期待どおりの結果が得られます。

0
1
2
done

これは、コードの最も内側のループのみを停止することが重要です。したがって、複数のループを使用すると、望ましくない動作が発生する可能性があります。

for (auto j = 0; true; ++j)
    for (auto i = 0; true; ++i) {
        if (i == 3)
            break;
        std::cout << i << '\n';
    }
std::cout << "done";

このコードでは、上記の例と同じ結果を取得したかったのですが、代わりに無限ループが発生しました。これは、breakiのループではなく、jのループのみを停止するためです。

テストを行う:

0
1
2
0
1
2
...

実際のアプリケーションでbreak条件をどのように使用しますか?

通常、breakは、内部ループの一部をスキップするか、ループ出口を追加するためにのみ使用されます。

たとえば、素数をテストする関数では、現在の数が素数でない場合を見つけたらすぐに、残りの実行をスキップするためにそれを使用します。

auto is_prime = true;
for (auto i = 0; i < p; ++i) {
    if (p%i == 0) { //p is dividable by i!
        is_prime = false;
        break; //we already know that p is not prime, therefore we do not need to test more cases!
    }

または、文字列のベクトルを検索する場合、通常、ループヘッドにデータの最大サイズを入れ、実際に検索しているデータが見つかった場合は、追加の条件を使用してループを終了します。

auto j = size_t(0);
for (auto i = size_t(0); i < data.size(); ++i)
    if (data[i] == "Hello") { //we found "Hello"!
        j = i;
        break; //we already found the string, no need to search any further!
    }

3. return条件を使用する

returnキーワードは、現在のスコープを終了し、呼び出し元の関数に戻ります。したがって、ループの終了に使用でき、さらに、呼び出し元に番号を返すことができます。一般的なケースは、returnを使用してループ(およびその関数)を終了し、結果を返すことです。

たとえば、is_prime上からの関数:

auto inline is_prime(int p) {
    for (auto i = 0; i < p; ++i)
        if (p%i == 0) //p is dividable by i!
            return false; //we already know that p is not prime, and can skip the rest of the cases and return the result
    return true; //we didn't find any divisor before, thus p must be prime!
}

returnキーワードを使用して、複数のループを終了することもできます。

auto inline data_has_match(std::vector<std::string> a, std::vector<std::string> b) {
    for (auto i = size_t(0); i < a.size(); ++i)
        for (auto j = size_t(0); j < a.size(); ++j)
            if (a[i] == b[j])
                return true; //we found a match! nothing to do here
    return false; //no match was found
}

実際のアプリケーションでreturn条件をどのように使用しますか?

小さな関数の内部では、returnがループを終了して直接結果を返すためによく使用されます。さらに、より大きな関数の内部では、returnはコードを明確で読みやすくするのに役立ちます。

for (auto i = 0; i < data.size(); ++i) {
    //do some calculations on the data using only i and put them inside result
    if (is_match(result,test))
        return result;
    for (auto j = 0; j < i; ++j) {
        //do some calculations on the data using i and j and put them inside result
        if (is_match(result,test))
            return result;
    }
}
return 0; //we need to return something in the case that no match was found

次のことよりも理解しやすいです:

auto break_i_loop = false;
auto return_value = 0;
for (auto i = 0; !break_i_loop; ++i) {
    //do some calculations on the data using only i and put them inside result
    if (is_match(result,test)) { //a match was found, save the result and break the loop!
        return_value = result;
        break;
    }
    for (auto j = 0; j < i; ++j) {
        //do some calculations on the data using i and j and put them inside result
        if (is_match(result,test)) { //a match was found, save the result, break the loop, and make sure that we break the outer loop too!
            return_value = result;
            break_i_loop = true;
            break;
        }
    }
    if (!break_i_loop) //if we didn't find a match, but reached the end of the data, we need to break the outer loop
        break_i_loop = i >= data.size();
}
return return_value; //return the result

4.例外を使用する

例外は、コード内で例外的なイベントをマークする方法です。たとえば、ファイルからデータを読み取りたいが、何らかの理由でファイルが存在しない場合!例外を使用してループを終了できますが、通常、コンパイラは、例外が処理された場合にプログラムを安全に続行するための大量の定型コードを生成します。したがって、例外は非常に非効率的であるため、値を返すために使用しないでください。

実際のアプリケーションでどのように例外を使用しますか?

例外は、本当に例外的なケースを処理するために使用されます。たとえば、データの逆数を計算する場合、ゼロで除算しようとすることがあります。ただし、これは計算に役立ちません。したがって、次のように記述します。

auto inline inverse_data(std::vector<int>& data) {
    for (auto i = size_t(0); i < data.size(); ++i)
        if (data[i] == 0)
            throw std::string("Division by zero on element ") + std::to_string(i) + "!";
        else
            data[i] = 1 / data[i];
}

呼び出し関数内でこの例外を処理できます。

while (true)
    try {
        auto data = get_user_input();
        inverse = inverse_data(data);
        break;
    }
    catch (...) {
        std::cout << "Please do not put zeros into the data!";
    }

dataにゼロが含まれる場合、inverse_dataは例外をスローし、breakは実行されず、ユーザーは再度データを入力する必要があります。

この種のエラー処理には、追加のエラータイプなど、さらに高度なオプションがありますが、これは別の日のトピックです。

**絶対にすべきでないこと! **

前述のように、例外は実行時のオーバーヘッドを大幅に増加させる可能性があります。したがって、それらは本当に例外的な場合にのみ使用する必要があります。次の関数を書くことは可能ですが、しないでください!

auto inline next_prime(int start) {
    auto p = start;
    try {
        for (auto i = start; true; ++i)
            if (is_prime(i)) {
                p = i;
                throw;
            }
   }
   catch (...) {}
   return p;
 }

5. gotoの使用

gotoキーワードは、コードを読みにくくし、意図しない副作用を引き起こす可能性があるため、ほとんどのプログラマーに嫌われています。ただし、(複数の)ループを終了するために使用できます。

for (auto j = 0; true; ++j)
    for (auto i = 0; true; ++i) {
        if (i == 3)
            goto endloop;
        std::cout << i << '\n';
    }
endloop:
std::cout << "done";

このループは終了し(パート2のループとは異なります)、出力されます。

0
1
2
done

実際のアプリケーションでgotoをどのように使用しますか?

99.9%の場合、gotoキーワードを使用する必要はありません。唯一の例外は、Arduinoなどの組み込みシステム、または非常に高性能なコードです。これら2つのいずれかを使用している場合は、gotoを使用して、より高速で効率的なコードを生成できます。ただし、日常のプログラマーにとっては、欠点はgotoを使用することの利点よりもはるかに大きくなります。

ケースが0.1%の1つであると思われる場合でも、gotoが実際に実行を改善するかどうかを確認する必要があります。たいていの場合、breakまたはreturn条件を使用すると、コンパイラーがgotoを含むコードを理解するのが難しくなります。

1
jan.sende