Ce traitement de int64_t est-il un bogue GCC AND Clang?

Maintenant, certains d’entre vous seront tentés de crier un comportement indéfini , mais il y a un problème. Le type int64_t n’est PAS défini par le standard C mais par POSIX . POSIX définit ce type comme suit:

un type entier signé avec une largeur N, pas de bits de remplissage et une représentation à complément à deux.

Cela ne laisse pas cela pour l’implémentation à définir et ne permet surtout pas de le traiter comme un entier non limité.

 linux$ cat xc #include  #include  #include  int stupid (int64_t a) { return (a+1) > a; } int main(void) { int v; printf("%d\n", v = stupid(INT64_MAX)); exit(v); } linux$ gcc -ox xc -Wall && ./x 0 linux$ gcc -ox xc -Wall -O2 && ./x # THIS IS THE ERROR. 1 linux$ gcc --version gcc (Debian 4.9.2-10) 4.9.2 Copyright (C) 2014 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. linux$ uname -a Linux localhost 3.14.13-0-amd64 #1 SMP Sat Jul 26 20:03:23 BST 2014 x86_64 GNU/Linux linux$ getconf LONG_BIT 32 linux$ 

De toute évidence, il y a un problème ici … qu’est-ce que c’est?
Ai-je manqué une sorte de dissortingbution implicite?

Vous n’avez pas besoin de passer à POSIX pour résoudre ce problème, ISO C contrôle cet aspect particulier dans son ensemble (les références ci-dessous correspondent au standard C11 ).

Le rest de cette réponse ira à tous les «juristes de la langue» pour montrer pourquoi leur comportement n’est pas défini pour en append un à votre valeur signée et donc pourquoi les deux réponses (vraie et fausse) sont valides.


Tout d’abord, votre affirmation selon laquelle int64_t n’est pas défini dans ISO n’est pas vraiment correcte. Section 7.20.1.1 Exact-width integer types indiquent, lorsque l’on se réfère aux types intN_t :

Le nom typedef int N _t désigne un type entier signé avec une largeur N , aucun bit de remplissage et une représentation de complément à deux. Ainsi, int8_t désigne un tel type entier signé avec une largeur de exactement 8 bits.

Ces types sont facultatifs. Cependant, si une implémentation fournit des types entiers avec des largeurs de 8, 16, 32 ou 64 bits, pas de bits de remplissage et (pour les types signés) qui ont une représentation complémentaire de deux, elle doit définir les noms typedef correspondants.

C’est pourquoi vous n’avez pas à vous soucier de la définition de ces types de POSIX, car ISO les définit exactement de la même manière (complément à deux, pas de remplissage, etc.) en supposant qu’il possède les capacités appropriées.


Donc, maintenant que nous avons établi que ISO les définit (s’ils sont disponibles dans l’implémentation), regardons maintenant 6.5 Expressions /5 :

Si une condition exceptionnelle se produit lors de l’évaluation d’une expression (c’est-à-dire si le résultat n’est pas défini mathématiquement ou ne figure pas dans la plage des valeurs représentables pour son type), le comportement n’est pas défini.

L’ajout de deux types intégraux identiques vous donnera certainement le même type (au moins au rang de int64_t , bien au-dessus du point où les promotions entières sont effectuées 1 ), puisque cela est dicté par les conversions arithmétiques habituelles spécifiées au 6.3.1.8 . Après la section traitant de divers types de virgule flottante (dont int64_t n’est pas), on voit:

Si les deux opérandes ont le même type, aucune autre conversion n’est nécessaire.

Plus haut dans cette même section, vous trouvez l’instruction qui dicte le type du résultat une fois qu’un type commun a été trouvé:

Sauf mention contraire explicite, le type réel commun est également le type réel correspondant du résultat.

Donc, étant donné que le résultat de INT64_MAX+1 ne correspond pas vraiment à une variable int64_t , le comportement est indéfini.


D’après vos commentaires, l’encodage de int64_t indique que l’ajout de celui ci se ferait , il faut comprendre que cela ne change pas la clause dont elle n’est pas définie du tout. Une implémentation est toujours libre de faire ce qu’elle veut dans ce cas, même si cela n’a pas de sens en fonction de ce que vous pensez.

Et, de toute façon, l’expression INT64_MAX + 1 > INT64_MAX (où 1 subit la promotion entière en int64_t ) peut très bien être simplement compilée en 1 car cela est sans doute plus rapide que d’incrémenter réellement une valeur et de faire une comparaison. Et c’est le bon résultat, puisque tout est le résultat correct 🙂

En ce sens, ce n’est pas différent d’une implémentation convertissant:

 int ub (int i) { return i++ * i++; } // big no-no : int x = ub (3); 

dans le plus simple et presque certainement plus rapide:

 int x = 0; 

Vous pouvez prétendre que la réponse serait mieux comme 9 ou 12 (selon le moment où les effets secondaires ++ sont effectués) mais, étant donné que le comportement indéfini est une rupture du contrat entre le codeur et le compilateur, le compilateur est libre de faire ce qu’il veut .


Dans tous les cas, si vous voulez une version bien définie de votre fonction, vous pouvez simplement opter pour quelque chose comme:

 int stupid (int64_t a) { return (a == INT64_MAX) ? 0 : 1; } 

Cela vous permet d’obtenir le résultat souhaité / attendu sans avoir recours à un comportement indéfini 🙂


1 Il peut y avoir un cas d’arête si la largeur de int est en réalité supérieure à 64 bits. Dans ce cas, il se peut que les promotions en nombres entiers obligent int64_t à un int , ce qui permet de bien définir l’expression. Je ne me suis pas penché sur cette question en détail, donc je me trompe peut-être (en d’autres termes, ne le considérez pas comme une partie évangélique de ma réponse), mais cela vaut la peine de garder à l’esprit que plus de 64 bits de large.

Je vais encore crier UN COMPORTEMENT NON DÉFINI .

Le raisonnement est simple, le compilateur suppose que vous êtes un programmeur parfait et que vous n’écrirez jamais de code pouvant entraîner un comportement indéfini.

Alors, quand il voit votre fonction:

 int stupid (int64_t a) { return (a+1) > a; } 

Cela suppose que vous ne l’appellerez jamais avec a==INT64_MAX , car ce serait UB.

Par conséquent, cette fonction peut être sortingvialement optimisée pour:

 int stupid (int64_t a) { return 1; } 

Ce qui peut ensuite être intégré si nécessaire.

Je vous suggère de lire Ce que chaque programmeur devrait savoir sur le comportement indéfini pour plus d’explications sur la manière dont les compilateurs exploitent UB à des fins d’optimisation.

POSIX définit la largeur et la représentation de int64_t , mais le comportement des opérateurs arithmétiques tels que + est défini par le standard C. Le standard C indique clairement que le comportement des opérateurs arithmétiques sur les valeurs signées n’est pas défini si le résultat déborde.

C11 6.5 §5 s’applique:

Si une condition exceptionnelle se produit lors de l’évaluation d’une expression (c’est-à-dire si le résultat n’est pas défini mathématiquement ou ne figure pas dans la plage des valeurs représentables pour son type), le comportement n’est pas défini.

Ce n’est pas la définition du type qui pose problème (même sans POSIX, les types entiers de largeur exacte sont déjà spécifiés pour utiliser le complément à deux par le standard C seul), mais l’opération «addition signée» se traduit par une exception condition.

Si vous ne le voulez pas, utilisez plutôt une arithmétique non signée. Notez que si vous souhaitez éviter le comportement défini par l’implémentation, la conversion finale en signature devra être effectuée via un type-punning au lieu de la diffusion.

Personnellement, je pense que c’est exagéré et serait bien d’écrire la comparaison comme

 (int64_t)((uint64_t)a + 1) > a 

au lieu de passer par le tracas de l’utilisation des syndicats ou des moulages de pointeurs.

La spécification de la représentation complémentaire de 2 est importante car C fournit des opérations arithmétiques et binarys sur le même type. Il est donc important de définir la relation entre la représentation binary d’une variable et son interprétation arithmétique.

Par exemple, définir le bit le plus significatif d’un entier de complément à 2 sur 1 (opération sur le bit) équivaut à soustraire INT_MAX+1 de sa valeur (opération arithmétique).

Ces opérations ne sont pas définies les unes par rapport aux autres, mais elles sont définies selon la logique mathématique. Il n’y a pas de complément à “2”, car “complément à 2” est un concept de représentation en bits, et “addition” est un concept arithmétique – les termes proviennent de domaines différents.

En tant que tel, la définition de ces relations ne définit pas automatiquement le résultat d’opérations individuelles qui, dans leur propre domaine, génèrent des valeurs en dehors de la plage du type. Par exemple, un décalage de bits qui nécessite plus de bits que ce qui a été défini dans la représentation est indéfini, quelle que soit sa relation avec la valeur arithmétique si elle était possible. Dans le domaine de la représentation binary, l’opération est tout simplement impossible.

De même, une addition résultant en une valeur arithmétique supérieure à la valeur maximale autorisée par le type est indéfinie, quelles que soient les opérations sur les bits qui seraient effectuées si elle était tentée. Dans le domaine de l’arithmétique, le résultat est bien défini, mais impossible à stocker.

Il est possible pour un standard de définir un comportement pour des opérations qui sont logiquement “impossibles”, par exemple une division par zéro. En cas de dépassement de capacité, un standard pourrait définir que cela entraîne un “wraparound”, affectant les bits les plus significatifs de la représentation. De même, il pourrait dire que cette opération devrait entraîner une erreur d’exécution quelconque ou la définir comme résultant en une valeur maximale possible.

Le standard C laisse plutôt cela comme un comportement indéfini, ce qui signifie que tout comportement choisi par un compilateur est également valide, même si cela varie selon les différents niveaux d’optimisation ou les différentes parties d’un programme. Dans votre cas, il n’y a pas de valeur de a pour laquelle affirmer qu’un a+1 > a est incorrect , le compilateur est donc libre de supposer que l’inégalité est vraie pour toutes les valeurs possibles et de la simplifier au moment de la compilation.