web-dev-qa-db-fra.com

Pourquoi C ++ promeut-il un int en un flottant quand un flottant ne peut pas représenter toutes les valeurs int?

Dites que j'ai ce qui suit:

int i = 23;
float f = 3.14;
if (i == f) // do something

i sera promu en float et les deux float seront comparés, mais un float peut-il représenter toutes les int valeurs? Pourquoi ne pas promouvoir à la fois le int et le float en double?

67
user4344762

Lorsque int est promu unsigned dans les promotions intégrales, les valeurs négatives sont également perdues (ce qui conduit à un plaisir tel que 0u < -1 étant vrai).

Comme la plupart des mécanismes en C (hérités en C++), les conversions arithmétiques habituelles doivent être comprises en termes d'opérations matérielles. Les créateurs de C connaissaient très bien le langage d'assemblage des machines avec lesquelles ils travaillaient, et ils ont écrit C pour donner un sens immédiat à eux-mêmes et aux gens comme eux-mêmes lorsqu'ils écrivaient des choses qui auraient été écrites jusque-là en Assemblée (comme l'UNIX noyau).

Maintenant, les processeurs, en règle générale, n'ont pas d'instructions de type mixte (ajoutez float à double, comparez int à float, etc.) car ce serait un énorme gaspillage de biens immobiliers sur la plaquette - vous devez implémenter autant d'opcodes que vous le souhaitez pour prendre en charge différents types. Le fait que vous n'ayez que des instructions pour "ajouter un entier à un entier", "comparer un flottant à un flottant", "multiplier un non signé par un non signé", etc. rend les conversions arithmétiques habituelles nécessaires en premier lieu - elles sont un mappage de deux types à l'instruction famille qui a le plus de sens à utiliser avec eux.

Du point de vue de quelqu'un qui a l'habitude d'écrire du code machine de bas niveau, si vous avez des types mixtes, les instructions d'assembleur que vous êtes le plus susceptible de considérer dans le cas général sont celles qui nécessitent le moins de conversions. C'est particulièrement le cas avec les virgules flottantes, où les conversions sont coûteuses à l'exécution, et en particulier au début des années 1970, lorsque C a été développé, les ordinateurs étaient lents et lorsque les calculs en virgule flottante étaient effectués par logiciel. Cela apparaît dans les conversions arithmétiques habituelles - un seul opérande est jamais converti (à la seule exception de long/unsigned int, où long peut être converti en unsigned long, ce qui n'exige rien sur la plupart des machines. Peut-être pas là où l'exception s'applique).

Ainsi, les conversions arithmétiques habituelles sont écrites pour faire ce qu'un codeur d'assemblage ferait la plupart du temps: vous avez deux types qui ne correspondent pas, convertissez l'un en l'autre pour qu'il le fasse. C'est ce que vous feriez dans le code assembleur, sauf si vous aviez une raison spécifique de faire autrement, et aux personnes habituées à écrire du code assembleur et do ont une raison spécifique pour forcer une conversion différente, explicitement demander que la conversion soit naturelle. Après tout, vous pouvez simplement écrire

if((double) i < (double) f)

Il est intéressant de noter dans ce contexte, en passant, que unsigned est plus élevé dans la hiérarchie que int, de sorte que la comparaison de int avec unsigned se terminent par une comparaison non signée (d'où le 0u < -1 bit depuis le début). Je soupçonne que c'est un indicateur que les gens d'autrefois considéraient unsigned moins comme une restriction sur int que comme une extension de sa plage de valeurs: nous n'avons pas besoin du signe pour le moment, donc utilisons le bit supplémentaire pour une plus grande plage de valeurs. Vous l'utiliseriez si vous aviez des raisons de vous attendre à ce qu'un int déborde - une préoccupation bien plus grande dans un monde de ints 16 bits.

68
Wintermute

Même double peut ne pas être en mesure de représenter toutes les valeurs de int, selon la quantité de bits que contient int.

Pourquoi ne pas promouvoir à la fois l'int et le float en double?

Probablement parce qu'il est plus coûteux de convertir les deux types en double que d'utiliser l'un des opérandes, qui est déjà un float, comme float. Il introduirait également des règles spéciales pour les opérateurs de comparaison incompatibles avec les règles pour les opérateurs arithmétiques.

Il n'y a également aucune garantie sur la façon dont les types à virgule flottante seront représentés, il serait donc aveugle de supposer que la conversion de int en double (ou même long double) pour comparaison résoudra tout.

13
doc

Les règles de promotion de type sont conçues pour être simples et fonctionner de manière prévisible. Les types en C/C++ sont naturellement "triés" par la plage de valeurs qu'ils peuvent représenter. Voir this pour plus de détails. Bien que les types à virgule flottante ne puissent pas représenter tous les entiers représentés par des types intégraux car ils ne peuvent pas représenter le même nombre de chiffres significatifs, ils peuvent représenter une plage plus large.

Pour avoir un comportement prévisible, lorsque vous avez besoin de promotions de type, les types numériques sont toujours convertis en type avec la plage plus grande pour éviter un débordement dans le plus petit. Imagine ça:

int i = 23464364; // more digits than float can represent!
float f = 123.4212E36f; // larger range than int can represent!
if (i == f) { /* do something */ }

Si la conversion était effectuée vers le type intégral, le float f déborderait certainement lors de la conversion en int, conduisant à un comportement indéfini. En revanche, la conversion de i en f ne provoque qu'une perte de précision qui n'est pas pertinente car f a la même précision, il est donc toujours possible que la comparaison réussisse. C'est au programmeur à ce stade d'interpréter le résultat de la comparaison en fonction des exigences de l'application.

Enfin, outre le fait que les nombres à virgule flottante double précision souffrent du même problème de représentation des nombres entiers (nombre limité de chiffres significatifs), l'utilisation de la promotion sur les deux types entraînerait une représentation de précision plus élevée pour i, tandis que f est voué à avoir la précision d'origine, donc la comparaison ne réussira pas si i a des chiffres plus significatifs que f pour commencer. Maintenant, c'est aussi un comportement non défini: la comparaison peut réussir pour certains couples (i, f) mais pas pour d'autres.

10
Giovanni Botta

un float peut-il représenter toutes les valeurs de int?

Pour un système moderne typique où int et float sont stockés sur 32 bits, non. Quelque chose doit donner. La valeur de 32 bits d'entiers ne correspond pas à 1 sur 1 sur un ensemble de même taille qui comprend des fractions.

Le i sera promu en float et les deux float seront comparés…

Pas nécessairement. Vous ne savez pas vraiment quelle précision s'appliquera. C++ 14 §5/12:

Les valeurs des opérandes flottants et les résultats des expressions flottantes peuvent être représentés avec une précision et une plage plus grandes que celles requises par le type; les types n'en sont pas modifiés.

Bien que i après la promotion ait le type nominal float, la valeur peut être représentée à l'aide du matériel double. C++ ne garantit pas la perte ou le dépassement de précision en virgule flottante. (Ce n'est pas nouveau en C++ 14; il est hérité de C depuis les temps anciens.)

Pourquoi ne pas promouvoir à la fois le int et le float en double?

Si vous voulez une précision optimale partout, utilisez à la place double et vous ne verrez jamais un float. Ou long double, mais cela pourrait fonctionner plus lentement. Les règles sont conçues pour être relativement sensées pour la majorité des cas d'utilisation de types à précision limitée, étant donné qu'une machine peut offrir plusieurs précisions alternatives.

La plupart du temps, rapide et lâche est suffisant, la machine est donc libre de faire ce qui est le plus facile. Cela peut signifier une comparaison arrondie, simple précision, ou double précision et aucun arrondi.

Mais, de telles règles sont finalement des compromis, et parfois elles échouent. Pour spécifier précisément l'arithmétique en C++ (ou C), cela aide à rendre explicites les conversions et les promotions. De nombreux guides de style pour les logiciels extra-fiables interdisent complètement les conversions implicites, et la plupart des compilateurs proposent des avertissements pour vous aider à les supprimer.

Pour en savoir plus sur la façon dont ces compromis sont survenus, vous pouvez parcourir le document de justification C . (La dernière édition couvre jusqu'à C99.) Ce n'est pas seulement un bagage insensé de l'époque du PDP-11 ou du K&R.

8
Potatoswatter

Il est fascinant qu'un certain nombre de réponses ici proviennent de l'origine du langage C, nommant explicitement K&R et les bagages historiques comme la raison pour laquelle un int est converti en flotteur lorsqu'il est combiné avec un flotteur.

Cela pointe le blâme sur les mauvaises parties. Dans K&R C, il n'y avait pas de calcul flottant. Toutes les opérations en virgule flottante ont été effectuées en double précision. Pour cette raison, un entier (ou toute autre chose) n'a jamais été implicitement converti en flottant, mais seulement en double. Un flottant ne pouvait pas non plus être le type d'un argument de fonction: vous deviez passer un pointeur pour flotter si vous vouliez vraiment, vraiment, vraiment éviter la conversion en double. Pour cette raison, les fonctions

int x(float a)
{ ... }

et

int y(a)
float a;
{ ... }

ont différentes conventions d'appel. Le premier obtient un argument flottant, le second (désormais interdit en tant que syntaxe) obtient un double argument.

Les arguments arithmétiques et fonctionnels à virgule flottante simple précision n'ont été introduits qu'avec ANSI C. Kernighan/Ritchie est innocent.

Maintenant, avec le flotteur unique nouvellement disponible expressions (le flotteur unique n'était auparavant qu'un format de stockage), il devait également y avoir de nouvelles conversions de type. Ce que l'équipe ANSI C a choisi ici (et je serais à perte pour un meilleur choix) n'est pas la faute de K&R.

6
user4467309

Q1: Un flottant peut-il représenter toutes les valeurs int?

IEE754 peut représenter tous les entiers exactement comme des flottants, jusqu'à environ 223, comme mentionné dans cette réponse .

Q2: Pourquoi ne pas promouvoir à la fois l'int et le float en double?

Les règles de la norme pour ces conversions sont de légères modifications de celles de K&R: les modifications tiennent compte des types ajoutés et des règles de préservation de la valeur. Une licence explicite a été ajoutée pour effectuer des calculs dans un type "plus large" qu'il n'est absolument nécessaire, car cela peut parfois produire un code plus petit et plus rapide, sans parler plus souvent de la bonne réponse. Les calculs peuvent également être effectués dans un type "plus étroit" par la règle comme si tant que le même résultat final est obtenu. La conversion explicite peut toujours être utilisée pour obtenir une valeur dans le type souhaité.

Source

L'exécution de calculs dans un type plus large signifie que, étant donné float f1; et float f2;, f1 + f2 peut être calculé avec une précision de double. Et cela signifie que étant donné int i; et float f;, i == f peut être calculé avec une précision de double. Mais il n'est pas nécessaire de calculer i == f en double précision, comme l'indique hvd dans le commentaire.

La norme C le dit également. Ce sont les conversions arithmétiques habituelles. La description suivante est tirée directement de la norme ANSI C.

... si l'un des opérandes a le type float, l'autre opérande est converti en type float.

Source et vous pouvez le voir dans le ref aussi.

Un lien pertinent est ceci réponse . Une source plus analytique est ici .

Voici une autre façon d'expliquer cela: Les conversions arithmétiques habituelles sont implicitement effectuées pour convertir leurs valeurs dans un type commun. Le compilateur effectue d'abord la promotion des nombres entiers, si les opérandes ont encore des types différents, ils sont convertis en le type qui apparaît le plus haut dans la hiérarchie suivante:

enter image description here

Source .

5
gsamaras

Lorsqu'un langage de programmation est créé, certaines décisions sont prises intuitivement.

Par exemple, pourquoi ne pas convertir int + float en int + int au lieu de float + float ou double + double? Pourquoi appeler int-> float une promotion si elle contient le même nombre de bits? Pourquoi ne pas appeler float-> int une promotion?

Si vous comptez sur des conversions de types implicites, vous devez savoir comment elles fonctionnent, sinon convertissez-les manuellement.

Certains langages auraient pu être conçus sans aucune conversion de type automatique. Et toutes les décisions au cours d'une phase de conception n'auraient pas pu être prises logiquement pour une bonne raison.

JavaScript avec sa frappe de canard a des décisions encore plus obscures sous le capot. Concevoir un langage absolument logique est impossible, je pense que cela va au théorème d'incomplétude de Godel. Vous devez équilibrer la logique, l'intuition, la pratique et les idéaux.

3
exebook

Les règles sont écrites pour des bits de 16 bits (la plus petite taille requise). Votre compilateur à 32 bits convertit sûrement les deux côtés en double. Il n'y a pas de registre flottant dans le matériel moderne de toute façon donc a pour convertir en double. Maintenant, si vous avez des entrées 64 bits, je ne suis pas trop sûr de ce qu'il fait. un double long serait approprié (normalement 80 bits mais ce n'est même pas standard).

2
Joshua

La question est pourquoi: parce qu'il est rapide, facile à expliquer, facile à compiler, et ce sont toutes des raisons très importantes au moment où le langage C a été développé.

Vous auriez pu avoir une règle différente: que pour chaque comparaison de valeurs arithmétiques, le résultat est celui de la comparaison des valeurs numériques réelles. Cela se situerait quelque part entre trivial si l'une des expressions comparées est une constante, une instruction supplémentaire lors de la comparaison d'un entier signé et non signé, et assez difficile si vous comparez long long et double et que vous voulez des résultats corrects lorsque le long long ne peut pas être représenté comme double. (0u <-1 serait faux, car cela comparerait les valeurs numériques 0 et -1 sans tenir compte de leurs types).

Dans Swift, le problème est résolu facilement en interdisant les opérations entre différents types.

2
gnasher729