Question # 1: Déclarer une variable dans une boucle est-il une bonne ou une mauvaise pratique?
J'ai lu les autres discussions sur l'existence ou non d'un problème de performances (la plupart ont dit non) et sur le fait que vous devriez toujours déclarer les variables aussi proches de l'endroit où elles vont être utilisées. Ce que je me demande, c'est si cela devrait ou non être évité ou plutôt préféré.
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 ignore-t-elle simplement cette partie, ou crée-t-elle réellement un emplacement en mémoire à chaque fois?
C'est excellent pratique.
En créant des variables à l'intérieur de boucles, vous vous assurez que leur étendue est limitée à l'intérieur de la boucle. Il ne peut être ni 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 ultérieurement dans votre code (peut également être atténué en utilisant l'avertissement -Wshadow
instruction 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 réalisée plus efficacement par le compilateur (l’allocation de registre par dessus tout), car il sait que la variable ne peut pas être utilisée en dehors de la boucle. Par exemple, il n'est pas nécessaire de stocker le résultat pour une utilisation ultérieure.
En bref, vous avez raison de le faire.
Notez cependant que la variable est non 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 n ° 2: la variable est allouée une fois, lorsque la fonction est appelée. En fait, du point de vue de l'allocation, cela revient (presque) à 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 se peut même que la variable ne soit pas allouée, il suffit de réutiliser un créneau libre (à partir d'une autre variable dont la portée est terminée).
Avec une portée restreinte et plus précise viennent des optimisations plus précises. Mais plus important encore, cela sécurise votre code, 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'un bloc if(){...}
. Typiquement, au lieu de:
int result;
(...)
result = f1();
if (result) then { (...) }
(...)
result = f2();
if (result) then { (...) }
il est plus prudent 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’ya plus de risque de transporter une certaine valeur result
de f1()
à f2()
. Chaque result
est strictement limitée à sa propre portée, ce qui rend son rôle plus précis. Du point de vue des réviseurs, il est beaucoup plus agréable, car il a moins de variables d’état à longue portée à se soucier de et à suivre.
Même le compilateur aidera mieux: en supposant qu'à l'avenir, après un changement de code erroné, result
ne soit pas correctement initialisé avec f2()
. La deuxième version refusera tout simplement de fonctionner en indiquant un message d'erreur clair au moment de la compilation (bien mieux que l'exécution). La première version ne détectera rien, le résultat de f1()
sera simplement testé une seconde fois, confondu avec le résultat de f2()
.
L'outil open source CppCheck (un outil d'analyse statique pour le code C/C++) fournit d'excellentes indications sur 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 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’existe pas de construction en C, l’espace pour la variable sera simplement alloué dans la pile (sans aucune initialisation) lors de l’appel de la fonction. 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 que je connais beaucoup moins. Je suppose que l'allocation ne constituera probablement pas un problème, car le compilateur doit être suffisamment 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, c'est une très bonne pratique de le garder très proche.
Dans certains cas, une considération telle que la performance 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 de petite chaîne (SSO), de sorte que l'allocation dynamique peut ê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 retirer la constante:
const std::string testing = "testing";
for (int counter = 0; counter <= 10; counter++) {
cout << testing;
}
La plupart des compilateurs réalisent-ils que la variable a déjà été déclarée et sautent-ils cette partie ou crée-t-elle réellement un emplacement en mémoire à chaque fois?
Il peut réutiliser l’espace variable consommé et extraire les 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 objet (tel que std::string
). Dans le cas du std::string
, cet espace contient un pointeur contenant 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, ainsi qu'une allocation dynamique et libre si la variable se situe au-dessus du seuil de décompte de caractères SSO (et que 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 que l'implémentation doit vérifier 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 prendre en compte lorsque le contenu de la chaîne ou du vecteur varie.
Alors, que faites-vous avec toutes ces options (et plus)? Conservez-le très proche par défaut - jusqu'à ce que vous compreniez bien les coûts et sachiez quand vous devriez 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<<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 chaque destructeur de boucle ont besoin de 5 secondes pour terminer.
Vous aurez besoin de 5 secondes jusqu'à ce que vous obteniez la sortie de myOtherFunc.
Bien sûr, ceci est un exemple fou.
Mais cela montre que cela peut devenir un problème de performances lorsque chaque boucle effectue la même construction lorsque 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:
{
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 réalisez (ce que je n'avais pas fait quand j'ai commencé la programmation), que les crochets (tant qu'ils sont par paires) peuvent être placés n'importe où dans le code, pas juste après "si", "pour", " tandis que ", etc.
Mon code compilé dans Microsoft Visual C++ 2010 Express, je sais donc que cela fonctionne; De plus, j'ai essayé d'utiliser la variable en dehors des crochets dans lesquels elle avait été définie et j'ai reçu une erreur. Je sais donc que la variable a été "détruite".
Je ne sais pas si c'est une mauvaise pratique d'utiliser cette méthode, car de nombreux crochets non étiquetés pourraient rapidement rendre le code illisible, mais certains commentaires pourraient peut-être éclaircir les choses.