Déclaration de variables à l’intérieur de boucles, bonnes pratiques ou mauvaises pratiques?

Question n ° 1: La déclaration d’une variable dans une boucle est-elle une bonne pratique ou une mauvaise pratique?

J’ai lu les autres threads pour savoir s’il existe ou non un problème de performance (la plupart dit non), et que vous devez toujours déclarer les variables aussi près que possible de leur utilisation. Ce que je me demande, c’est si cela doit être évité ou non.

Exemple:

for(int counter = 0; counter <= 10; counter++) { string someString = "testing"; cout << someString; } 

Question n ° 2: La plupart des compilateurs réalisent-ils que la variable a déjà été déclarée et sautent simplement cette partie, ou crée-t-elle réellement un spot en mémoire à chaque fois?

C’est une excellente pratique.

En créant des variables à l’intérieur des boucles, vous vous assurez que leur scope est limitée à l’intérieur de la boucle. Il ne peut pas être référencé ni appelé en dehors de la boucle.

Par ici:

  • Si le nom de la variable est un peu “générique” (comme “i”), il n’y a pas de risque de le mélanger avec une autre variable du même nom plus tard dans votre code (peut aussi être atténuée avec l’instruction -Wshadow sur GCC )

  • Le compilateur sait que la scope de la variable est limitée à l’intérieur de la boucle et émettra donc un message d’erreur correct si la variable est appelée par erreur ailleurs

  • Last but not least, une optimisation dédiée peut être effectuée plus efficacement par le compilateur (surtout une allocation de registre), car elle sait que la variable ne peut pas être utilisée en dehors de la boucle. Par exemple, pas besoin de stocker le résultat pour une réutilisation ultérieure.

En bref, vous avez raison de le faire.

Notez cependant que la variable n’est pas supposée conserver sa valeur entre chaque boucle. Dans ce cas, vous devrez peut-être l’initialiser à chaque fois. Vous pouvez également créer un bloc plus grand, englobant la boucle, dont le seul but est de déclarer les variables qui doivent conserver leur valeur d’une boucle à une autre. Cela inclut généralement le compteur de boucles lui-même.

 { int i, retainValue; for (i=0; i 

Pour la question n ° 2: La variable est allouée une fois, lorsque la fonction est appelée. En fait, du sharepoint vue de l'allocation, c'est presque la même chose que de déclarer la variable au début de la fonction. La seule différence est la scope: la variable ne peut pas être utilisée en dehors de la boucle. Il est même possible que la variable ne soit pas allouée, en réutilisant simplement un emplacement libre (à partir d'une autre variable dont la scope est terminée).

Avec une scope restreinte et plus précise, des optimisations plus précises sont possibles. Mais plus important encore, cela rend votre code plus sûr, avec moins d'états (variables) à prendre en compte lors de la lecture d'autres parties du code.

Cela est vrai même en dehors d'une boucle if(){...} . En règle générale, au lieu de:

  int result; (...) result = f1(); if (result) then { (...) } (...) result = f2(); if (result) then { (...) } 

il est plus sûr d'écrire:

  (...) { int const result = f1(); if (result) then { (...) } } (...) { int const result = f2(); if (result) then { (...) } } 

La différence peut sembler mineure, surtout sur un si petit exemple. Mais sur une base de code plus grande, cela aidera: maintenant, il n'y a plus de risque de transporter une valeur de result du bloc f1() au bloc f2() . Chaque result est ssortingctement limité à sa propre scope, ce qui rend son rôle plus précis. Du sharepoint vue du relecteur, c'est beaucoup plus intéressant, car il a des variables d'état à longue scope dont il faut se soucier et suivre.

Même le compilateur aidera mieux: en supposant qu'à l'avenir, après un changement de code erroné, le result ne soit pas correctement initialisé avec f2() . La deuxième version refusera simplement de fonctionner, indiquant un message d'erreur clair au moment de la compilation (bien meilleur que le temps d'exécution). La première version ne détectera rien, le résultat de f1() sera simplement testé une seconde fois, confondant le résultat de f2() .

Information complémentaire

L'outil open source CppCheck (un outil d'parsing statique pour le code C / C ++) fournit d'excellents conseils concernant la scope optimale des variables.

En réponse à un commentaire sur l'allocation: La règle ci-dessus est vraie dans C, mais peut ne pas l'être pour certaines classes C ++.

Pour les types et les structures standard, la taille de la variable est connue au moment de la compilation. Il n'y a pas de "construction" en C, donc l'espace pour la variable sera simplement alloué dans la stack (sans aucune initialisation), quand la fonction est appelée. C'est pourquoi il y a un coût "zéro" lors de la déclaration de la variable dans une boucle.

Cependant, pour les classes C ++, il y a cette chose de constructeur dont je connais beaucoup moins. Je suppose que l'allocation ne sera probablement pas le problème, car le compilateur devra être assez intelligent pour réutiliser le même espace, mais l'initialisation aura probablement lieu à chaque itération de la boucle.

En règle générale, il est très judicieux de restr très proche.

Dans certains cas, il y aura une considération telle que la performance qui justifie de retirer la variable de la boucle.

Dans votre exemple, le programme crée et détruit la chaîne à chaque fois. Certaines bibliothèques utilisent une optimisation par petites chaînes (SSO), de sorte que l’allocation dynamic pourrait être évitée dans certains cas.

Supposons que vous vouliez éviter ces créations / allocations redondantes, vous écrivez comme suit:

 for (int counter = 0; counter <= 10; counter++) { // compiler can pull this out const char testing[] = "testing"; cout << testing; } 

ou vous pouvez tirer la constante:

 const std::ssortingng testing = "testing"; for (int counter = 0; counter <= 10; counter++) { cout << testing; } 

Est-ce que la plupart des compilateurs réalisent que la variable a déjà été déclarée et sautent simplement cette partie, ou crée-t-elle réellement un spot en mémoire à chaque fois?

Il peut réutiliser l'espace consommé par la variable et peut extraire des invariants de votre boucle. Dans le cas du tableau de caractères const (ci-dessus), ce tableau pourrait être extrait. Cependant, le constructeur et le destructeur doivent être exécutés à chaque itération dans le cas d'un object (tel que std::ssortingng ). Dans le cas du std::ssortingng , cet «espace» inclut un pointeur qui contient l'allocation dynamic représentant les caractères. Donc ça:

 for (int counter = 0; counter <= 10; counter++) { string testing = "testing"; cout << testing; } 

exigerait une copie redondante dans chaque cas, et une allocation dynamic et gratuite si la variable se situe au-dessus du seuil pour le nombre de caractères SSO (et que la SSO est implémentée par votre bibliothèque std).

Ce faisant:

 ssortingng testing; for (int counter = 0; counter <= 10; counter++) { testing = "testing"; cout << testing; } 

aurait toujours besoin d'une copie physique des caractères à chaque itération, mais le formulaire pourrait entraîner une allocation dynamic car vous affectez la chaîne et l'implémentation doit voir qu'il n'est pas nécessaire de redimensionner l'allocation de sauvegarde de la chaîne. Bien sûr, vous ne le feriez pas dans cet exemple (car de nombreuses alternatives supérieures ont déjà été démontrées), mais vous pourriez le considérer lorsque le contenu de la chaîne ou du vecteur varie.

Alors, que faites-vous avec toutes ces options (et plus)? Restez très proche par défaut - jusqu'à ce que vous compreniez bien les coûts et que vous sachiez à quel moment vous devez vous écarter.

Pour C ++, cela dépend de ce que vous faites. OK, c’est du code stupide mais imaginez

 class myTimeEatingClass { public: //constructor myTimeEatingClass() { sleep(2000); ms_usedTime+=2; } ~myTimeEatingClass() { sleep(3000); ms_usedTime+=3; } const unsigned int getTime() const { return ms_usedTime; } static unsigned int ms_usedTime; }; 
 myTimeEatingClass::ms_CreationTime=0; myFunc() { for (int counter = 0; counter <= 10; counter++) { myTimeEatingClass timeEater(); //do something } cout << "Creating class took "<< timeEater.getTime() <<"seconds at all< 

Vous allez attendre 55 secondes jusqu'à ce que vous obteniez la sortie de myFunc. Tout simplement parce que chaque constructeur et destructeur de boucle ont besoin de 5 secondes pour se terminer.

Vous aurez besoin de 5 secondes pour obtenir la sortie de myOtherFunc.

Bien sûr, c'est un exemple fou.

Mais cela montre que cela peut devenir un problème de performance quand chaque boucle de la même construction est faite quand le constructeur et / ou le destructeur ont besoin de temps.

Je n’ai pas posté pour répondre aux questions de JeremyRR (car elles ont déjà été répondues); Au lieu de cela, j’ai posté simplement pour donner une suggestion.

Pour JeremyRR, vous pouvez faire ceci:

 { ssortingng someSsortingng = "testing"; for(int counter = 0; counter <= 10; counter++) { cout << someString; } // The variable is in scope. } // The variable is no longer in scope. 

Je ne sais pas si vous réalisez (ce n'est pas le cas lorsque j'ai commencé à programmer), que les parenthèses (tant qu'elles sont en paires) peuvent être placées n'importe où dans le code, pas juste après "if", "for", " tandis que ", etc.

Mon code compilé dans Microsoft Visual C ++ 2010 Express, donc je sais que cela fonctionne; De plus, j'ai essayé d'utiliser la variable en dehors des parenthèses dans lesquelles elle était définie et j'ai reçu une erreur, donc je sais que la variable a été "détruite".

Je ne sais pas si c'est une mauvaise pratique d'utiliser cette méthode, car beaucoup de parenthèses sans étiquette pourraient rapidement rendre le code illisible, mais peut-être que certains commentaires pourraient clarifier les choses.