Déclarer des variables à l'intérieur des boucles, bonne ou mauvaise pratique?

266

Question # 1: La déclaration d'une variable à l'intérieur d'une boucle est-elle une bonne ou une mauvaise pratique?

J'ai lu les autres discussions pour savoir s'il y a ou non un problème de performances (la plupart ont dit non) et que vous devriez toujours déclarer les variables aussi près de l'endroit où elles vont être utilisées. Ce que je me demande, c'est si cela doit être évité ou si c'est réellement préféré.

Exemple:

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

   cout << someString;
}

Question # 2: La plupart des compilateurs se rendent-ils compte que la variable a déjà été déclarée et sautent-ils simplement cette partie, ou créent-ils réellement une place pour elle en mémoire à chaque fois?

JeramyRR
la source
29
Mettez-les près de leur utilisation, sauf indication contraire du profilage.
Mooing Duck
3
@drnewman J'ai lu ces fils de discussion, mais ils n'ont pas répondu à ma question. Je comprends que la déclaration de variables à l'intérieur des boucles fonctionne. Je me demande si c'est une bonne pratique de le faire ou si c'est quelque chose à éviter.
JeramyRR

Réponses:

348

C'est une excellente pratique.

En créant des variables à l'intérieur des boucles, vous vous assurez que leur portée 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 aucun risque de le mélanger avec une autre variable du même nom quelque part plus tard dans votre code (peut également être atténué en utilisant l' -Wshadowinstruction d'avertissement sur GCC)

  • Le compilateur sait que la portée de la variable est limitée à l'intérieur de la boucle et émettra donc un message d'erreur approprié si la variable est référencée par erreur ailleurs.

  • Enfin et surtout, une optimisation dédiée peut être effectuée plus efficacement par le compilateur (plus important encore, l'allocation des registres), car il 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.

Bref, vous avez raison de le faire.

Notez cependant que la variable n'est pas censé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 des variables qui doivent conserver leur valeur d'une boucle à l'autre. Cela inclut généralement le compteur de boucle lui-même.

{
    int i, retainValue;
    for (i=0; i<N; i++)
    {
       int tmpValue;
       /* tmpValue is uninitialized */
       /* retainValue still has its previous value from previous loop */

       /* Do some stuff here */
    }
    /* Here, retainValue is still valid; tmpValue no longer */
}

Pour la question # 2: La variable est allouée une fois, lorsque la fonction est appelée. En fait, du point de 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 portée: la variable ne peut pas être utilisée en dehors de la boucle. Il peut même être possible que la variable ne soit pas allouée, simplement en réutilisant un emplacement libre (à partir d'une autre variable dont la portée est terminée).

Une portée restreinte et plus précise s'accompagne d'optimisations plus précises. Mais plus important encore, cela rend votre code plus sûr, avec moins d'états (c'est-à-dire des variables) à prendre en compte lors de la lecture d'autres parties du code.

Cela est vrai même en dehors d'un if(){...}bloc. 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 aucun risque de transporter une resultvaleur de f1()à f2()bloquer. Chacun resultest strictement limité à sa propre portée, ce qui rend son rôle plus précis. Du point de vue de l'examinateur, c'est beaucoup plus agréable, car il a moins de variables d'état à longue portée à s'inquiéter et à suivre.

Même le compilateur aidera mieux: en supposant qu'à l'avenir, après un changement de code erroné, il resultne 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 mieux que lors de l'exécution). La première version ne verra rien, le résultat de f1()sera simplement testé une deuxième fois, confondu avec le résultat de f2().

Information complémentaire

L'outil open-source CppCheck (un outil d'analyse statique pour le code C / C ++) fournit d'excellentes astuces concernant la portée optimale des variables.

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

Pour les types et 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 pile (sans aucune initialisation), lorsque la fonction sera appelée. C'est pourquoi il y a un coût "nul" lors de la déclaration de la variable à l'intérieur d'une boucle.

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

Cyan
la source
4
Réponse géniale. C'est exactement ce que je cherchais, et m'a même donné un aperçu de quelque chose que je ne savais pas. Je ne savais pas que la portée reste à l'intérieur de la boucle uniquement. Merci pour votre réponse!
JeramyRR
22
"Mais il ne sera jamais plus lent que d'allouer au début de la fonction." Ce n'est pas toujours vrai. La variable sera allouée une fois, mais elle sera toujours construite et détruite autant de fois que nécessaire. Ce qui dans le cas de l'exemple de code est 11 fois. Pour citer le commentaire de Mooing "Mettez-les près de leur utilisation, sauf indication contraire du profilage."
IronMensan
4
@ JeramyRR: Absolument pas - le compilateur n'a aucun moyen de savoir si l'objet a des effets secondaires significatifs dans son constructeur ou destructeur.
ildjarn
2
@Iron: En revanche, lorsque vous déclarez l'élément en premier, vous recevez simplement de nombreux appels à l'opérateur d'affectation; qui coûte généralement à peu près le même prix que la construction et la destruction d'un objet.
Billy ONeal
4
@BillyONeal: Pour stringet plus vectorprécisément, l'opérateur d'affectation peut réutiliser le tampon alloué à chaque boucle, ce qui (en fonction de votre boucle) peut représenter un énorme gain de temps.
Mooing Duck
22

Généralement, c'est une très bonne pratique de le garder très près.

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 petite optimisation de chaîne (SSO), de sorte que l'allocation dynamique pourrait être évitée dans certains cas.

Supposons que vous vouliez éviter ces créations / allocations redondantes, vous l'écririez ainsi:

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

ou vous pouvez extraire la constante:

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

Est-ce que la plupart des compilateurs se rendent compte que la variable a déjà été déclarée et saute juste cette partie, ou crée-t-elle réellement une place pour elle en mémoire à chaque fois?

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

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

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

Ce faisant:

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

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

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

Justin
la source
1
En ce qui concerne les types de données de base comme float ou int, la déclaration de la variable à l'intérieur de la boucle sera-t-elle plus lente que la déclaration de cette variable à l'extérieur de la boucle car elle devra allouer un espace pour la variable à chaque itération?
Kasparov92
2
@ Kasparov92 La réponse courte est "Non. Ignorez cette optimisation et placez-la dans la boucle lorsque cela est possible pour une meilleure lisibilité / localité. Le compilateur peut effectuer cette micro-optimisation pour vous." Plus en détail, c'est au compilateur de décider en dernier ressort, en fonction de ce qui convient le mieux à la plate-forme, aux niveaux d'optimisation, etc. Un int / float ordinaire à l'intérieur d'une boucle sera généralement placé sur la pile. Un compilateur peut certainement déplacer cela en dehors de la boucle et réutiliser le stockage s'il y a une optimisation pour ce faire. Pour des raisons pratiques, ce serait une optimisation très très très petite…
justin
1
@ Kasparov92… (suite) que vous ne considéreriez que dans des environnements / applications où chaque cycle comptait. Dans ce cas, vous voudrez peut-être simplement envisager d'utiliser l'assemblage.
juste
14

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

}
myOtherFunc()
{
    myTimeEatingClass timeEater();
    for (int counter = 0; counter <= 10; counter++) {
        //do something
    }
    cout << "Creating class took "<< timeEater.getTime() <<"seconds at all<<endl;

}

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

Vous aurez besoin de 5 secondes jusqu'à ce que vous obteniez la sortie de myOtherFunc.

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

Mais cela montre que cela peut devenir un problème de performances lorsque chaque boucle a la même construction lorsque le constructeur et / ou le destructeur a besoin de temps.

Nobby
la source
2
Eh bien, techniquement, dans la deuxième version, vous obtiendrez la sortie en seulement 2 secondes, car vous n'avez pas encore détruit l'objet .....
Chrys
12

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

Pour JeremyRR, vous pouvez faire ceci:

{
  string someString = "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 vous rendez compte (je ne l'ai pas fait lorsque j'ai commencé la programmation), que les crochets (tant qu'ils sont en paires) peuvent être placés n'importe où dans le code, pas seulement après "si", "pour", " pendant ", etc.

Mon code compilé dans Microsoft Visual C ++ 2010 Express, donc je sais que cela fonctionne; aussi, j'ai essayé d'utiliser la variable en dehors des crochets dans lesquels 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 crochets non étiquetés pourraient rapidement rendre le code illisible, mais peut-être que certains commentaires pourraient clarifier les choses.

Fearnbuster
la source
4
Pour moi, c'est une réponse très légitime qui apporte une suggestion directement liée à la question. Vous avez mon vote!
Alexis Leclerc
0

C'est une très bonne pratique, car toutes les réponses ci-dessus fournissent un très bon aspect théorique de la question, permettez-moi de donner un aperçu du code, j'essayais de résoudre DFS sur GEEKSFORGEEKS, je rencontre le problème d'optimisation ...... Si vous essayez de résoudre le code déclarant l'entier en dehors de la boucle vous donnera une erreur d'optimisation.

stack<int> st;
st.push(s);
cout<<s<<" ";
vis[s]=1;
int flag=0;
int top=0;
while(!st.empty()){
    top = st.top();
    for(int i=0;i<g[top].size();i++){
        if(vis[g[top][i]] != 1){
            st.push(g[top][i]);
            cout<<g[top][i]<<" ";
            vis[g[top][i]]=1;
            flag=1;
            break;
        }
    }
    if(!flag){
        st.pop();
    }
}

Maintenant, mettez des entiers dans la boucle, cela vous donnera une réponse correcte ...

stack<int> st;
st.push(s);
cout<<s<<" ";
vis[s]=1;
// int flag=0;
// int top=0;
while(!st.empty()){
    int top = st.top();
    int flag = 0;
    for(int i=0;i<g[top].size();i++){
        if(vis[g[top][i]] != 1){
            st.push(g[top][i]);
            cout<<g[top][i]<<" ";
            vis[g[top][i]]=1;
            flag=1;
            break;
        }
    }
    if(!flag){
        st.pop();
    }
}

cela reflète complètement ce que monsieur @justin disait dans le deuxième commentaire .... essayez ceci ici https://practice.geeksforgeeks.org/problems/depth-first-traversal-for-a-graph/1 . il suffit de lui donner un coup de feu ... vous l'obtiendrez. J'espère que cette aide.

KhanJr
la source
Je ne pense pas que cela s'applique à la question. De toute évidence, dans votre cas ci-dessus, cela compte. La question portait sur le cas où la définition de variable pouvait être définie ailleurs sans changer le comportement du code.
pcarter
Dans le code que vous avez publié, le problème n'est pas la définition mais la partie d'initialisation. flagdoit être réinitialisé à 0 à chaque whileitération. C'est un problème de logique, pas un problème de définition.
Martin Véronneau
0

Chapitre 4.8 Structure des blocs dans K&R Le langage de programmation C 2.Ed. :

Une variable automatique déclarée et initialisée dans un bloc est initialisée à chaque entrée du bloc.

J'ai peut-être manqué de voir la description pertinente dans le livre comme:

Une variable automatique déclarée et initialisée dans un bloc n'est allouée qu'une seule fois avant l'entrée du bloc.

Mais un simple test peut prouver l'hypothèse retenue:

 #include <stdio.h>                                                                                                    

 int main(int argc, char *argv[]) {                                                                                    
     for (int i = 0; i < 2; i++) {                                                                                     
         for (int j = 0; j < 2; j++) {                                                                                 
             int k;                                                                                                    
             printf("%p\n", &k);                                                                                       
         }                                                                                                             
     }                                                                                                                 
     return 0;                                                                                                         
 }                                                                                                                     
sof
la source