J'ai récemment rencontré du code qui a une boucle du formulaire
for (int i = 0; i < 1e7; i++){
}
Je remets en question la sagesse de le faire car 1e7 est un type à virgule flottante, et provoquera la promotion de i
lors de l'évaluation de la condition d'arrêt. Cela devrait-il être préoccupant?
l'éléphant dans la pièce ici est que la plage d'un int
pourrait être aussi petite que -32767 à +32767, et le comportement lors de l'attribution d'une valeur supérieure à celle-ci à un tel int
est non défini .
Mais, en ce qui concerne votre point principal, en effet, cela devrait vous concerner car c'est une très mauvaise habitude. Les choses pourraient mal tourner car oui, 1e7 est un type double à virgule flottante.
Le fait que i
sera converti en virgule flottante en raison des règles de promotion de type est quelque peu théorique: le vrai dommage est fait s'il y a une troncature inattendue du littéral intégral apparent. A titre de "preuve par l'exemple", considérons d'abord la boucle
for (std::uint64_t i = std::numeric_limits<std::uint64_t>::max() - 1024; i ++< 18446744073709551615ULL; ){
std::cout << i << "\n";
}
Cela génère chaque valeur consécutive de i
dans la plage, comme vous vous en doutez. Notez que std::numeric_limits<std::uint64_t>::max()
est 18446744073709551615ULL
, Ce qui est 1 de moins que la 64e puissance de 2. (Ici, j'utilise un "opérateur" de type diapositive ++<
Qui est utile lorsque vous travaillez avec des types unsigned
. Beaucoup de gens considèrent -->
et ++<
comme obscurcissant mais dans la programmation scientifique, ils sont courants, en particulier -->
.)
Maintenant sur ma machine, un double est un point flottant IEEE754 64 bits. (Un tel schéma est particulièrement efficace pour représenter des puissances de 2 exactement - IEEE754 peut représenter des puissances de 2 jusqu'à 1022 exactement.) Ainsi, 18,446,744,073,709,551,616
(La 64e puissance de 2) peut être représenté exactement comme un double. Le nombre représentable le plus proche avant cela est 18,446,744,073,709,550,592
(Qui est 1024 de moins).
Alors maintenant, écrivons la boucle comme
for (std::uint64_t i = std::numeric_limits<std::uint64_t>::max() - 1024; i ++< 1.8446744073709551615e19; ){
std::cout << i << "\n";
}
Sur ma machine, cela ne produira que n valeur de i
: 18,446,744,073,709,550,592
(Le nombre que nous avons déjà vu). Cela prouve que 1.8446744073709551615e19
Est un type à virgule flottante. Si le compilateur était autorisé à traiter le littéral comme un type intégral, la sortie des deux boucles serait équivalente.
Cela fonctionnera, en supposant que votre int
est au moins 32 bits.
Cependant, si vous voulez vraiment utiliser la notation exponentielle, vous devriez mieux définir une constante entière en dehors de la boucle et utiliser une conversion appropriée, comme ceci:
const int MAX_INDEX = static_cast<int>(1.0e7);
...
for (int i = 0; i < MAX_INDEX; i++) {
...
}
Compte tenu de cela, je dirais qu'il vaut mieux écrire
const int MAX_INDEX = 10000000;
ou si vous pouvez utiliser C++ 14
const int MAX_INDEX = 10'000'000;
1e7
est un littéral de type double
, et généralement double
est au format IEEE 754 64 bits avec une mantisse 52 bits. Environ chaque dixième puissance de 2 correspond à une troisième puissance de 10, donc double
devrait être capable de représenter des entiers jusqu'à au moins 105 * 3 = 1015, exactement. Et si int
est 32 bits, alors int
a environ 103 * 3 = 109 comme valeur maximale (la recherche Google indique que "2 ** 31 - 1" = 2 147 483 647, soit le double de l'estimation approximative).
Donc, dans la pratique, il est sûr sur les systèmes de bureau actuels et plus grands.
Mais C++ autorise int
à seulement 16 bits, et par exemple un système embarqué avec ce petit int
, on aurait un comportement indéfini.
Si l'intention de boucler pour un nombre entier exact d'itérations, par exemple si itérer sur exactement tous les éléments d'un tableau, comparer avec une valeur à virgule flottante n'est peut-être pas une si bonne idée, uniquement pour des raisons de précision; comme la conversion implicite d'un entier en flottant tronquera les entiers vers zéro, il n'y a pas de danger réel d'accès hors des limites, il abandonnera simplement la boucle courte.
Maintenant, la question est: quand ces effets commencent-ils réellement? Votre programme en fera-t-il l'expérience? La représentation en virgule flottante habituellement utilisée de nos jours est IEEE 754. Tant que l'exposant est égal à 0, une valeur en virgule flottante est essentiellement un entier. La double précision C flotte sur 52 bits pour la mantisse, ce qui vous donne une précision entière jusqu'à une valeur allant jusqu'à 2 ^ 52, qui est de l'ordre d'environ 1e15. Sans spécifier avec un suffixe f
que vous voulez qu'un littéral à virgule flottante soit interprété en simple précision, le littéral sera en double précision et la conversion implicite le ciblera également. Donc, tant que votre condition de fin de boucle est inférieure à 2 ^ 52, cela fonctionnera fiable!
Maintenant, une question à laquelle vous devez penser sur l'architecture x86 est l'efficacité. Les tout premiers FPU 80x87 sont venus dans un package différent, et plus tard une puce différente et comme résultat d'obtenir des valeurs dans les registres FPU est un peu gênant au niveau de l'assemblage x86. Selon vos intentions, cela peut faire la différence dans l'exécution pour une application en temps réel; mais c'est une optimisation prématurée.
TL; DR: Est-il sûr de le faire? Très certainement oui. Cela causera-t-il des problèmes? Cela pourrait causer des problèmes numériques. Peut-il invoquer un comportement non défini? Cela dépend de la façon dont vous utilisez la condition de fin de boucle, mais si i
est utilisé pour indexer un tableau et pour une raison quelconque, la longueur du tableau s'est retrouvée dans une variable à virgule flottante toujours tronquée vers zéro, cela ne causera pas de problème logique. Est-ce une chose intelligente à faire? Cela dépend de l'application.