Je ne vois presque jamais une boucle for
comme ceci:
for (int i = 0; 5 != i; ++i)
{}
Existe-t-il une raison technique d'utiliser >
ou <
au lieu de !=
lors de l’incrémentation de 1 dans une boucle for
? Ou est-ce plus une convention?
while (time != 6:30pm) {
Work();
}
Il est 18h31 ... Zut, ma prochaine chance de rentrer chez moi, c'est demain! :)
Cela montre que la restriction plus stricte atténue les risques et est probablement plus intuitive à comprendre.
Il n'y a pas de raison technique. Mais il existe une atténuation des risques, une facilité de maintenance et une meilleure compréhension du code.
<
ou >
sont des restrictions plus fortes que !=
et remplissent exactement le même but dans la plupart des cas (je dirais même dans tous les cas pratiques).
Il y a une double question ici ; et un intéressant réponse .
Oui, il y a une raison. Si vous écrivez une boucle (basée sur un index ancien) comme celle-ci
for (int i = a; i < b; ++i){}
alors cela fonctionne comme prévu pour toutes les valeurs de a
et b
(c'est-à-dire zéro itérations lorsque a > b
au lieu d’infini si vous aviez utilisé i == b;
).
D'autre part, pour les itérateurs vous écrivez
for (auto it = begin; it != end; ++it)
parce que tout itérateur doit implémenter un operator!=
_, mais pas pour chaque itérateur, il est possible de fournir un operator<
.
Également basé sur les boucles pour les boucles
for (auto e : v)
ne sont pas seulement du sucre fantaisie, mais ils réduisent considérablement les chances d’écrire un code erroné.
Vous pouvez avoir quelque chose comme
for(int i = 0; i<5; ++i){
...
if(...) i++;
...
}
Si votre variable de boucle est écrite par le code interne, le i!=5
pourrait ne pas rompre cette boucle. C’est plus sûr de vérifier les inégalités.
Edit à propos de la lisibilité. La forme d'inégalité est beaucoup plus fréquemment utilisée. C'est donc très rapide à lire car il n'y a rien de spécial à comprendre (la charge cérébrale est réduite car la tâche est courante). Il est donc bon que les lecteurs utilisent ces habitudes.
Et dernier point mais non le moindre, on l’appelle programmation défensive, ce qui signifie de toujours prendre le plus fort des cas pour éviter les erreurs actuelles et futures qui influenceraient le programme.
Le seul cas où une programmation défensive n'est pas nécessaire est celui où les États ont été prouvés par des conditions préalables et postérieures (mais prouvant qu'il s'agit du programme le plus défensif de tous les programmes).
Je dirais qu'une expression comme
for ( int i = 0 ; i < 100 ; ++i )
{
...
}
est plus exprimant l'intention que ne l'est
for ( int i = 0 ; i != 100 ; ++i )
{
...
}
Le premier indique clairement que la condition est un test pour une limite supérieure exclusive sur une plage; ce dernier est un test binaire d'une condition de sortie. Et si le corps de la boucle n'est pas trivial, il est possible que l'index ne soit modifié que dans l'instruction for
.
Les itérateurs sont un cas important lorsque vous utilisez le plus souvent la notation !=
:
for(auto it = vector.begin(); it != vector.end(); ++it) {
// do stuff
}
Accordé: dans la pratique, j'écrirais la même chose en me basant sur un range-for
:
for(auto & item : vector) {
// do stuff
}
mais le point demeure: on compare normalement les itérateurs en utilisant ==
ou !=
.
La condition de boucle est un invariant de boucle appliqué.
Supposons que vous ne regardiez pas le corps de la boucle:
for (int i = 0; i != 5; ++i)
{
// ?
}
dans ce cas, vous savez au début de l'itération de la boucle que i
n'est pas égal à 5
.
for (int i = 0; i < 5; ++i)
{
// ?
}
dans ce cas, vous savez au début de l'itération de la boucle que i
est inférieur à 5
.
La seconde est beaucoup, beaucoup plus d'informations que la première, non? Maintenant, l’intention du programmeur est (presque certainement) la même chose, mais si vous recherchez des bogues, avoir confiance en la lecture d’une ligne de code est une bonne chose. Et le second applique cet invariant, ce qui signifie que certains bugs qui vous piqueraient dans le premier cas ne peuvent tout simplement pas se produire (ou ne causent pas de corruption de mémoire, par exemple) dans le second cas.
Vous en savez plus sur l'état du programme, en lisant moins de code, avec <
qu'avec !=
. Et sur les processeurs modernes, ils prennent le même temps qu’aucune différence.
Si votre i
n'a pas été manipulé dans le corps de la boucle, et il a toujours été augmenté de 1, et il a commencé moins que 5
, il n'y aurait pas de différence. Mais pour savoir si cela a été manipulé, vous devez confirmer chacun de ces faits.
Certains de ces faits sont relativement faciles, mais vous pouvez vous tromper. Vérifier tout le corps de la boucle est cependant une douleur.
En C++, vous pouvez écrire un type indexes
tel que:
for( const int i : indexes(0, 5) )
{
// ?
}
fait la même chose que l'une ou l'autre des deux boucles ci-dessus for
, même jusqu'au compilateur pour l'optimiser au même code. Ici, cependant, vous savez que i
ne peut pas être manipulé dans le corps de la boucle, car il est déclaré const
, sans que le code ne corrompe la mémoire.
Plus vous pouvez obtenir d'informations sur une ligne de code sans avoir à comprendre le contexte, plus il est facile de localiser ce qui ne va pas. <
dans le cas des boucles d’entier, vous obtiendrez plus d’informations sur l’état du code sur cette ligne que !=
Est-ce que.
Il peut arriver que la variable i
soit définie sur une valeur élevée et que vous utilisiez simplement le paramètre !=
opérateur, vous vous retrouverez dans une boucle sans fin.
Oui; OpenMP ne parallélise pas les boucles avec le !=
_ condition.
Comme déjà dit par Ian Newson, vous ne pouvez pas boucler de manière fiable sur une variable flottante et sortir avec !=
. Par exemple,
for (double x=0; x!=1; x+=0.1) {}
sera effectivement bouclé pour toujours, car 0.1 ne peut pas être représenté exactement en virgule flottante, le compteur manque donc de justesse 1. Avec <
il se termine.
(Notez cependant que c'est fondamentalement comportement indéfini si vous obtenez 0.9999 ... comme dernier numéro accepté - quel type de viole l'hypothèse inférieure à - ou déjà terminé à 1.0000000000000001.)
Comme vous pouvez le constater parmi les nombreuses autres réponses, il existe des raisons d'utiliser <au lieu de! =, Ce qui vous aidera dans les cas Edge, les conditions initiales, la modification involontaire du compteur de boucle, etc.
Honnêtement cependant, je ne pense pas que vous puissiez souligner suffisamment l’importance de la convention. Pour cet exemple, il sera assez facile pour les autres programmeurs de voir ce que vous essayez de faire, mais cela entraînera une double-prise. L'un des emplois de la programmation est de le rendre aussi lisible et familier que possible pour tout le monde. Ainsi, lorsque quelqu'un doit mettre à jour/modifier votre code, il ne faut pas beaucoup d'effort pour comprendre ce que vous faisiez dans différents blocs de code. . Si je voyais quelqu'un utiliser !=
, Je suppose qu'il y avait une raison pour laquelle ils l'ont utilisé à la place de <
et si c'était une grande boucle, j'examinerais tout en essayant de comprendre ce que vous avez fait pour que cela soit nécessaire ... et c'est une perte de temps.
Par "technique", je veux dire par adjectival, le comportement du langage/les bizarreries et les effets secondaires du compilateur, tels que les performances du code généré.
À cette fin, la réponse est: non (*). Le (*) est "veuillez consulter le manuel de votre processeur". Si vous travaillez avec un système Edge-case RISC ou FPGA, vous devrez peut-être vérifier les instructions générées et leur coût. Mais si vous utilisez à peu près n'importe quelle architecture moderne conventionnelle, il n'y a pas de différence de coût significative entre les processeurs entre lt
, eq
, ne
et gt
.
Si vous utilisez un cas Edge, vous pourriez trouver que !=
nécessite trois opérations (cmp
, not
, beq
) vs deux (cmp
, blt xtr myo
). Encore une fois, RTM dans ce cas.
Pour la plupart, les raisons sont défensives/durcissantes, en particulier lorsque vous travaillez avec des pointeurs ou des boucles complexes. Considérer
// highly contrived example
size_t count_chars(char c, const char* str, size_t len) {
size_t count = 0;
bool quoted = false;
const char* p = str;
while (p != str + len) {
if (*p == '"') {
quote = !quote;
++p;
}
if (*(p++) == c && !quoted)
++count;
}
return count;
}
Un exemple moins artificiel serait celui où vous utilisez des valeurs de retour pour effectuer des incréments, en acceptant les données d'un utilisateur:
#include <iostream>
int main() {
size_t len = 5, step;
for (size_t i = 0; i != len; ) {
std::cout << "i = " << i << ", step? " << std::flush;
std::cin >> step;
i += step; // here for emphasis, it could go in the for(;;)
}
}
Essayez ceci et entrez les valeurs 1, 2, 10, 999.
Vous pourriez empêcher ceci:
#include <iostream>
int main() {
size_t len = 5, step;
for (size_t i = 0; i != len; ) {
std::cout << "i = " << i << ", step? " << std::flush;
std::cin >> step;
if (step + i > len)
std::cout << "too much.\n";
else
i += step;
}
}
Mais ce que vous vouliez probablement, c'était
#include <iostream>
int main() {
size_t len = 5, step;
for (size_t i = 0; i < len; ) {
std::cout << "i = " << i << ", step? " << std::flush;
std::cin >> step;
i += step;
}
}
Il y a aussi une sorte de parti pris conventionnel envers <
, car la commande dans des conteneurs standard repose souvent sur operator<
, par exemple, le hachage dans plusieurs conteneurs STL détermine l’égalité en disant
if (lhs < rhs) // T.operator <
lessthan
else if (rhs < lhs) // T.operator < again
greaterthan
else
equal
Si lhs
et rhs
sont une classe définie par l'utilisateur écrivant ce code comme
if (lhs < rhs) // requires T.operator<
lessthan
else if (lhs > rhs) // requires T.operator>
greaterthan
else
equal
Le réalisateur doit fournir deux fonctions de comparaison. Alors <
est devenu l'opérateur privilégié.
Il y a plusieurs façons d'écrire n'importe quel type de code (généralement), il se trouve qu'il y en a deux dans ce cas (trois si vous comptez <= et> =).
Dans ce cas, les utilisateurs préfèrent> et <pour s’assurer que même si quelque chose d’inattendu se produit dans la boucle (comme un bogue), la boucle ne sera pas bouclée à l’infini (BAD). Considérez le code suivant, par exemple.
for (int i = 1; i != 3; i++) {
//More Code
i = 5; //OOPS! MISTAKE!
//More Code
}
Si nous utilisions (i <3), nous serions à l'abri d'une boucle infinie car elle imposait une restriction plus importante.
C'est vraiment votre choix si vous voulez une erreur dans votre programme de tout fermer ou de continuer à fonctionner avec le bogue là.
J'espère que cela a aidé!
La raison la plus courante d'utiliser <
est la convention. De plus en plus de programmeurs considèrent ces boucles comme "tant que l'index est dans la plage" plutôt que "jusqu'à ce que l'index atteigne la fin". Il vaut la peine de rester fidèle à la convention quand vous le pouvez.
D'autre part, beaucoup de réponses prétendent que l'utilisation de la <
forme aide à éviter les bugs. Je dirais que dans de nombreux cas, cela aide simplement masquer bugs. Si l'index de boucle est censé atteindre la valeur finale et au lieu de le dépasser, il se produit alors quelque chose d'inattendu qui pourrait provoquer un dysfonctionnement (ou être un effet secondaire d'un autre bogue). Le <
va probablement retarder la découverte du bogue. Le !=
_ est plus susceptible de provoquer un blocage, un blocage ou même un crash, ce qui vous aidera à détecter le bogue plus tôt. Plus tôt un bogue est trouvé, moins il est facile de le réparer.
Notez que cette convention est particulière à l'indexation par tableaux et vecteurs. Lorsque vous parcourez presque n'importe quel autre type de structure de données, vous devez utiliser un itérateur (ou un pointeur) et rechercher directement une valeur finale. Dans ces cas, vous devez vous assurer que l'itérateur atteindra et ne dépassera pas la valeur finale réelle.
Par exemple, si vous parcourez une chaîne C simple, il est généralement plus courant d'écrire:
for (char *p = foo; *p != '\0'; ++p) {
// do something with *p
}
que
int length = strlen(foo);
for (int i = 0; i < length; ++i) {
// do something with foo[i]
}
D'une part, si la chaîne est très longue, la deuxième forme sera plus lente car le strlen
est un autre passage à travers la chaîne.
Avec std :: string en C++, vous utiliseriez une boucle for basée sur une plage, un algorithme standard ou des itérateurs, même si la longueur est facilement disponible. Si vous utilisez des itérateurs, la convention est d'utiliser !=
plutôt que <
, un péché:
for (auto it = foo.begin(); it != foo.end(); ++it) { ... }
De même, itérer un arbre, une liste ou un mot de passe implique généralement de rechercher un pointeur ou une autre sentinelle null plutôt que de vérifier si un index reste dans une plage.
Il existe deux raisons liées de suivre cette pratique, toutes deux liées au fait qu'un langage de programmation est, après tout, un langage qui sera lu par des humains (entre autres).
(1) Un peu de redondance. En langage naturel, nous fournissons généralement plus d'informations que ce qui est strictement nécessaire, un peu comme un code de correction d'erreur. Ici, l’information supplémentaire est que la variable de boucle i
(voyez comment j’ai utilisé la redondance ici? Si vous ne saviez pas ce que signifie "variable de boucle", ou si vous avez oublié le nom de la variable, après avoir lu "boucle la variable i
"vous avez toutes les informations) est inférieure à 5 pendant la boucle et pas uniquement différente de 5. La redondance améliore la lisibilité.
(2) Convention. Les langues ont des manières standard spécifiques d'exprimer certaines situations. Si vous ne suivez pas la manière établie de dire quelque chose, vous serez toujours compris, mais l'effort pour le destinataire de votre message est plus important car certaines optimisations ne fonctionneront pas. Exemple:
Ne parlez pas autour de la purée chaude. Juste éclairer la difficulté!
La première phrase est une traduction littérale d'un idiome allemand. La seconde est un idiome anglais commun avec les mots principaux remplacés par des synonymes. Le résultat est compréhensible mais prend beaucoup plus de temps à comprendre que cela:
Ne pas tourner autour du pot. Expliquez simplement le problème!
Cela est vrai même dans le cas où les synonymes utilisés dans la première version correspondent mieux à la situation que les mots conventionnels de l'idiome anglais. Des forces similaires sont en vigueur lorsque les programmeurs lisent du code. C'est aussi pour cette raison que 5 != i
et 5 > i
sont des façons étranges de le dire sauf si vous travaillez dans un environnement dans lequel il est standard d’échanger le plus normal i != 5
et i < 5
de cette façon. De telles communautés dialectales existent, probablement parce que la cohérence facilite la mémorisation d'écriture 5 == i
au lieu du naturel mais sujet aux erreurs i == 5
.
Une des raisons pour ne pas utiliser cette construction est les nombres à virgule flottante. !=
est une comparaison très dangereuse à utiliser avec des flottants, car elle sera rarement évaluée comme vraie, même si les chiffres se ressemblent. <
ou >
_ supprime ce risque.
Utiliser des comparaisons relationnelles dans de tels cas est plus une habitude populaire qu'autre chose. Il a gagné en popularité à l'époque où des considérations conceptuelles telles que les catégories d'itérateurs et leur comparabilité n'étaient pas considérées comme hautement prioritaires.
Je dirais que l'on devrait préférer utiliser des comparaisons d'égalité plutôt que des comparaisons relationnelles chaque fois que cela est possible, car les comparaisons d'égalité imposent moins d'exigences aux valeurs comparées. Être EqualityComparable est une exigence moindre que d'être LessThanComparable.
Un autre exemple qui démontre l’applicabilité plus large de la comparaison d’égalité dans de tels contextes est l’énigme populaire avec la mise en œuvre de unsigned
itération jusqu’à 0
. Cela peut être fait comme
for (unsigned i = 42; i != -1; --i)
...
Notez que ce qui précède s'applique également aux itérations signée et non signée, tandis que la version relationnelle se décompose en types non signés.
Outre les exemples, où la variable de boucle changera (non intentionnellement) à l'intérieur du corps, il existe d'autres raisons d'utiliser les opérateurs plus petit que ou plus grand que:
<
ou >
n'est qu'un caractère, mais !=
deuxOutre les différentes personnes qui ont mentionné le fait que cela atténue les risques, cela réduit également le nombre de surcharges de fonctions nécessaires pour interagir avec divers composants de bibliothèque standard. Par exemple, si vous voulez que votre type soit stockable dans un std::set
, ou utilisé comme clé pour std::map
, ou utilisé avec certains algorithmes de recherche et de tri, la bibliothèque standard utilise généralement std::less
pour comparer des objets car la plupart des algorithmes n’ont besoin que d’un ordre faible strict. Ainsi, il devient une bonne habitude d’utiliser le <
comparaisons au lieu de !=
_ comparaisons (où cela a du sens, bien sûr).
Il n’ya pas de problème du point de vue de la syntaxe, mais la logique derrière cette expression 5!=i
n'est pas sonore.
À mon avis, en utilisant !=
définir les limites d'une boucle for n'est pas logiquement correct, car une boucle incrémente ou décrémente l'index d'itération. Par conséquent, définir la boucle pour qu'il soit itéré jusqu'à ce que l'index d'itération devienne hors limites (!=
à quelque chose) n’est pas une implémentation appropriée.
Cela fonctionnera, mais il est sujet à des problèmes de comportement car la gestion des données de limites est perdue lors de l'utilisation de !=
pour un problème incrémental (c’est-à-dire que vous savez dès le début s’il augmente ou diminue), c’est pourquoi, au lieu de !=
le <>>==>
sont utilisés.