web-dev-qa-db-fra.com

Sortir d'une boucle de l'intérieur d'une fonction appelée dans cette boucle

J'essaie actuellement de trouver un moyen de sortir d'une boucle for à partir d'une fonction appelée dans cette boucle. Je suis conscient de la possibilité que la fonction renvoie simplement une valeur, puis vérifie une valeur particulière, puis se casse, mais j'aimerais le faire directement depuis la fonction. C'est parce que j'utilise une bibliothèque interne pour un matériel spécifique qui oblige la signature de fonction de ma fonction à ressembler à ceci:

void foo (int passV, int aVal, long bVal)

Je suis conscient que le fait de ne pas utiliser de valeur de retour est très mauvaise pratique mais hélas, les circonstances m'y obligent, alors soyez indulgent avec moi.

Prenons l'exemple suivant:

#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;
}

Maintenant, cela ne compile pas. Au lieu de cela, j'obtiens une erreur de compilation comme suit:

prog.c: Dans la fonction 'foo': prog.c: 6: 2: erreur: instruction break pas dans la boucle ou break switch;

Je sais ce que cela signifie (le compilateur dit que la rupture dans foo() n'est pas dans une boucle for)

Maintenant, ce que j'ai pu trouver dans la norme concernant la déclaration de rupture est le suivant:

L'instruction break fait passer le contrôle à l'instruction suivant l'inclusion la plus interne while, do, for ou switch. La syntaxe est simplement break;

Étant donné que ma fonction est appelée à partir d'une boucle for, pourquoi l'instruction break ne s'échappe-t-elle pas de ladite boucle for? De plus, est-il possible de réaliser quelque chose comme ça sans que la fonction retourne d'abord?

29
Magisch

break, comme goto, ne peut sauter que localement dans la même fonction, mais si vous devez absolument le faire, vous pouvez utiliser setjmp et longjmp:

#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;
}

L'appel à longjmp provoquera un retour à l'appel setjmp. La valeur de retour de setjmp indique si elle revient après avoir défini la cible de saut, ou si elle revient d'un saut.

Production:

Inside foo!
Jumped out!

Les sauts non locaux sont sûrs lorsqu'ils sont utilisés correctement, mais il y a un certain nombre de choses auxquelles il faut réfléchir attentivement:

  • Puisque longjmp saute "à travers" toutes les activations de fonction entre l'appel setjmp et l'appel longjmp, si l'une de ces fonctions s'attend à pouvoir effectuer un travail supplémentaire après le courant lieu d'exécution, ce travail ne sera tout simplement pas fait.
  • Si l'activation de la fonction qui appelait setjmp s'est terminée, le comportement n'est pas défini. Tout peut arriver.
  • Si setjmp n'a pas encore été appelé, alors jump_target n'est pas défini et le comportement n'est pas défini.
  • Les variables locales de la fonction qui ont appelé setjmp peuvent dans certaines conditions avoir des valeurs non définies.
  • Un mot: fils.
  • D'autres éléments, tels que les indicateurs d'état à virgule flottante, peuvent ne pas être conservés et qu'il existe des restrictions sur l'endroit où vous pouvez placer l'appel setjmp.

La plupart d'entre eux suivent naturellement si vous avez une bonne compréhension de ce que fait un saut non local au niveau des instructions machine et des registres CPU, mais à moins que vous ne disposiez de cela, et ont lu ce que fait et fait la norme C pas garanti, je conseillerais une certaine prudence.

24

Vous ne pouvez pas utiliser break; De cette façon, il doit apparaître à l'intérieur du corps de la boucle for.

Il existe plusieurs façons de procéder, mais aucune n'est recommandée:

  • vous pouvez quitter le programme avec la fonction exit(). Puisque la boucle est exécutée à partir de main() et que vous ne faites rien après, il est possible de réaliser ce que vous voulez de cette façon, mais c'est un cas spécial.

  • Vous pouvez définir une variable globale dans la fonction et la tester dans la boucle for après l'appel de la fonction. L'utilisation de variables globales n'est généralement pas recommandée.

  • vous pouvez utiliser setjmp() et longjmp(), mais c'est comme essayer d'écraser une mouche avec un marteau, vous pouvez casser d'autres choses et rater complètement la mouche. Je ne recommanderais pas cette approche. De plus, il nécessite un jmpbuf que vous devrez passer à la fonction ou accéder en tant que variable globale.

Une alternative acceptable est de passer l'adresse d'une variable status comme argument supplémentaire: la fonction peut la définir pour indiquer la nécessité de rompre la boucle.

Mais de loin la meilleure approche en C est de renvoyer une valeur pour tester la continuation, c'est la plus lisible.

D'après vos explications, vous n'avez pas le code source de foo() mais pouvez détecter certaines conditions dans une fonction que vous pouvez modifier appelée directement ou indirectement par foo(): longjmp() sautez de son emplacement, profondément à l'intérieur des internes de foo(), peut-être plusieurs niveaux dans la pile d'appels, à l'emplacement setjmp(), en contournant le code de sortie de fonction normal pour tous les appels intermédiaires. Si c'est précisément ce que vous devez faire pour éviter un plantage, setjmp()/longjmp() est une solution, mais cela peut entraîner d'autres problèmes tels qu'une fuite de ressources, une initialisation manquante, un état incohérent et d'autres sources de comportement indéfini.

Notez que votre boucle for itérera 101 Fois car vous utilisez l'opérateur <=. La boucle idiomatique for utilise for (int i = 0; i < 100; i++) pour itérer exactement le nombre de fois qui apparaît comme limite supérieure (exclue).

44
chqrlie

(Remarque: la question a été modifiée depuis que j'ai écrit cela à l'origine)

En raison de la façon dont C est compilé, il doit savoir où s'arrêter lorsque la fonction est appelée. Puisque vous pouvez l'appeler de n'importe où, ou même d'un endroit où une coupure n'a aucun sens, vous ne pouvez pas avoir une instruction break; Dans votre fonction et la faire fonctionner comme ceci.

D'autres réponses ont suggéré des solutions terribles telles que la définition d'une variable globale, l'utilisation d'un #define Ou un saut long (!) Hors de la fonction. Ce sont des solutions extrêmement médiocres. Au lieu de cela, vous devez utiliser la solution que vous rejetez à tort dans votre premier paragraphe et renvoyer une valeur de votre fonction qui indique l'état dans lequel vous souhaitez déclencher un break et faire quelque chose comme ceci:

#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;
}

Essayer de trouver des moyens obscurs pour réaliser des choses comme ça au lieu d'utiliser la bonne façon d'obtenir le même résultat final est un moyen infaillible de générer un code de qualité terrible qui est un cauchemar à maintenir et à déboguer.

Vous mentionnez, caché dans un commentaire, que vous devez utiliser un retour nul, ce n'est pas un problème, passez simplement le paramètre break en tant que pointeur:

#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;
}

Étant donné que vos paramètres sont de type fixe, je vous suggère d'utiliser une distribution pour passer le pointeur à l'un des paramètres, par ex. foo(a, b, (long)&out);

9
Jack Aidley

break est une instruction qui est résolue pendant la compilation. Par conséquent, le compilateur doit trouver la boucle for/while appropriée dans la même fonction. Notez qu'il n'y a aucune garantie que la fonction ne puisse pas être appelée ailleurs.

8

Si vous ne pouvez pas utiliser l'instruction break, vous pouvez définir une variable locale dans votre module et ajouter une deuxième condition d'exécution à la boucle for. Par exemple, comme le code suivant:

#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;
}

Notez que dans cet exemple, il ne s'agit pas exactement d'une instruction break-, mais la boucle forloop ne fera pas une autre itération. Si vous voulez faire un break vous devez insérer une condition if- avec la variable continueLoop qui mène à break:

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

Si vous ne pouvez pas gérer les valeurs de retour, pouvez-vous au moins ajouter un paramètre à la fonction: je peux imaginer une solution comme celle-ci:

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;
  }
}

C'est mon premier article, alors pardonnez-moi, si ma mise en forme est foirée :)

4
Alexander Werner

C'est une autre idée qui peut ou non être réalisable: garder une variable autour qui peut transformer foo en 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;
}

Ceci est fonctionnellement le même sauf pour certaines pertes de cycles d'horloge (la fonction sera appelée, mais n'exécutera que l'instruction if), et il n'est pas nécessaire de changer la boucle. Ce n'est pas threadsafe et ne fonctionne que la première fois (mais foo pourrait réinitialiser la variable broken à 0 si elle est appelée avec a égale à 0, si nécessaire).

Donc pas génial, mais une idée qui n'a pas encore été mentionnée.

4
RemcoGerlich

Dans un cas comme celui-ci, envisagez d'utiliser une boucle while () avec plusieurs instructions conditionnelles chaînées avec && au lieu d'une boucle for. Bien que vous pouvez modifier le flux de contrôle normal en utilisant des fonctions comme setjmp et longjmp, c'est à peu près considéré comme une mauvaise pratique partout. Vous ne devriez pas avoir à chercher trop fort sur ce site pour savoir pourquoi. (En bref, c'est en raison de sa capacité à créer un flux de contrôle alambiqué qui ne se prête ni au débogage ni à la compréhension humaine)

Pensez également à faire quelque chose comme ceci:

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;
}

Dans ce cas, la boucle dépend d'une valeur vraie renvoyée par 'foo', ce qui rompra la boucle si la condition à l'intérieur de 'foo' n'est pas remplie.

Edit: je ne suis pas explicitement contre l'utilisation de goto, setjmp, longjmp etc. Mais je pense que dans ce cas, il existe une solution beaucoup plus simple et plus concise sans recourir à ces mesures!

4
ajxs

Suite à votre question mise à jour définissant clairement les limites, je vous suggère de déplacer la boucle entière à l'intérieur de votre fonction, puis d'appeler une deuxième fonction avec une valeur de retour à l'intérieur de cette fonction, par ex.

#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;
   }
}

Cela évite toute gymnastique extrême et fragile pour contourner la limitation.

3
Jack Aidley

Définissez simplement une variable globale et vérifiez-la sur la boucle:

#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

Envisagez d'inclure votre fonction manuellement dans la boucle for. Si cette fonction est appelée dans plusieurs boucles, définissez-la comme une macro:

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

Vous pouvez lancer une erreur dans votre fonction à l'intérieur de la boucle et intercepter cette erreur à l'extérieur de la boucle.

#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

Cette question a déjà été répondue, mais je pense qu'il vaut la peine de fouiller dans toutes les options possibles pour quitter une boucle en c ++. Il existe essentiellement cinq possibilités:

  • Utilisation d'une condition de boucle
  • Utilisation d'une condition break
  • Utilisation d'une condition return
  • Utilisation d'exceptions
  • Utilisation de goto

Dans ce qui suit, je décrirai les cas d'utilisation de ces options à l'aide de c ++ 14. Cependant, vous pouvez faire tout cela dans les versions antérieures de c ++ (sauf peut-être des exceptions). Pour faire court, je vais omettre les inclusions et la fonction principale. Veuillez commenter, si vous pensez qu'une partie a besoin de plus de clarté.

1. Utilisation d'une condition de boucle

La manière standard de quitter une boucle est une condition de boucle. La condition de boucle est écrite au milieu d'une instruction for, ou entre les parenthèses d'une instruction while:

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

La condition de boucle décide si la boucle doit être entrée et si la boucle doit être répétée. Dans tous les cas ci-dessus, la condition doit être true, pour que la boucle soit répétée.

Par exemple, si nous voulons sortir le nombre de 0 à 2, nous pourrions écrire le code en utilisant une boucle et une condition de boucle:

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

Ici, la condition est i <= 2. Tant que cette condition est évaluée à true, la boucle continue de fonctionner.

Une implémentation alternative serait de mettre la condition dans une variable à la place:

auto condition = false;

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

En vérifiant la sortie pour les deux versions, nous obtenons le résultat souhaité:

0
1
2
done

Comment utiliseriez-vous une condition de boucle dans une application réelle?

Les deux versions sont largement utilisées dans les projets c ++. Il est important de noter que la première version est plus compacte et donc plus facile à comprendre. Mais la deuxième version est généralement utilisée si la condition est plus complexe ou nécessite plusieurs étapes pour être évaluée.

Par exemple:

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. Utilisation d'une condition break

Une autre façon simple de quitter une boucle consiste à utiliser le mot clé break. S'il est utilisé à l'intérieur de la boucle, l'exécution s'arrêtera et continuera après le corps de la boucle:

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

Cela affichera le nombre actuel et l'incrémentera de un jusqu'à ce que i atteigne une valeur de 3. Ici, l'instruction if est notre condition break. Si la condition est true, la boucle est rompue (notez le !) et l'exécution se poursuit avec la ligne suivante, en imprimant done.

En faisant le test, on obtient en effet le résultat attendu:

0
1
2
done

Il est important que cela arrête uniquement la boucle la plus interne du code. Par conséquent, si vous utilisez plusieurs boucles, cela peut entraîner un comportement indésirable:

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

Avec ce code, nous voulions obtenir le même résultat que dans l'exemple ci-dessus, mais à la place, nous obtenons une boucle infinie, car break arrête uniquement la boucle sur i, et non celle sur j!

Faire le test:

0
1
2
0
1
2
...

Comment utiliseriez-vous une condition break dans une application réelle?

Généralement, break est uniquement utilisé pour ignorer des parties d'une boucle interne ou pour ajouter une sortie de boucle supplémentaire.

Par exemple, dans une fonction testant des nombres premiers, vous l'utiliseriez pour ignorer le reste de l'exécution, dès que vous avez trouvé un cas où le nombre actuel n'est pas premier:

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!
    }

Ou, si vous recherchez un vecteur de chaînes, vous placez généralement la taille maximale des données dans la tête de boucle et utilisez une condition supplémentaire pour quitter la boucle si vous avez réellement trouvé les données que vous recherchez.

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. Utilisation d'une condition return

Le mot clé return quitte la portée actuelle et revient à la fonction appelante. Ainsi, il peut être utilisé pour quitter les boucles et, en outre, redonner un numéro à l'appelant. Un cas courant consiste à utiliser return pour quitter une boucle (et sa fonction) et retourner un résultat.

Par exemple, nous pouvons réécrire le is_prime fonction d'en haut:

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!
}

Le mot clé return peut également être utilisé pour quitter plusieurs boucles:

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
}

Comment utiliseriez-vous une condition return dans une application réelle?

Dans les petites fonctions, return est souvent utilisé pour quitter les boucles et retourner directement les résultats. De plus, à l'intérieur de fonctions plus grandes, return aide à garder le code clair et lisible:

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

Ce qui est beaucoup plus facile à comprendre que:

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. Utilisation d'exceptions

Les exceptions sont un moyen de marquer des événements exceptionnels dans votre code. Par exemple, si vous souhaitez lire les données d'un fichier, mais pour une raison quelconque, le fichier n'existe pas! Les exceptions peuvent être utilisées pour quitter les boucles, mais le compilateur génère généralement beaucoup de code passe-partout pour continuer le programme en toute sécurité si l'exception est gérée. Par conséquent, les exceptions ne doivent pas être utilisées pour renvoyer des valeurs, car elles sont très inefficaces.

Comment utiliseriez-vous une exception dans une application du monde réel?

Les exceptions sont utilisées pour gérer des cas vraiment exceptionnels. Par exemple, si nous voulons calculer l'inverse de nos données, il peut arriver que nous essayions de diviser par zéro. Cependant, cela n'est pas utile dans notre calcul, nous écrivons donc:

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];
}

Nous pouvons gérer cette exception dans la fonction appelante:

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

Si data contient zéro, alors inverse_data lèvera une exception, le break n'est jamais exécuté et l'utilisateur doit saisir à nouveau les données.

Il existe encore plus d'options avancées pour ce type de gestion des erreurs, avec des types d'erreurs supplémentaires, ..., mais c'est un sujet pour un autre jour.

** Ce que vous ne devriez jamais faire! **

Comme mentionné précédemment, les exceptions peuvent produire une surcharge d'exécution importante. Par conséquent, ils ne devraient être utilisés que dans des cas vraiment exceptionnels. Bien qu'il soit possible d'écrire la fonction suivante, veuillez ne pas le faire!

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. Utilisation de goto

Le mot-clé goto est détesté par la plupart des programmeurs, car il rend le code plus difficile à lire et peut avoir des effets secondaires involontaires. Cependant, il peut être utilisé pour quitter (plusieurs) boucles:

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";

Cette boucle se terminera (pas comme la boucle de la partie 2) et produira:

0
1
2
done

Comment utiliseriez-vous un goto dans une application réelle?

Dans 99,9% des cas, il n'est pas nécessaire d'utiliser le mot clé goto. Les seules exceptions sont les systèmes embarqués, comme un Arduino, ou le code à très hautes performances. Si vous travaillez avec l'un de ces deux, vous souhaiterez peut-être utiliser goto pour produire du code plus rapide ou plus efficace. Cependant, pour le programmeur de tous les jours, les inconvénients sont beaucoup plus importants que les gains de l'utilisation de goto.

Même si vous pensez que votre cas fait partie des 0,1%, vous devez vérifier si le goto améliore réellement votre exécution. Plus souvent qu'autrement, l'utilisation d'une condition break ou return est plus rapide, car le compilateur a plus de mal à comprendre le code contenant goto.

1
jan.sende