J’utilise toujours unsigned int pour les valeurs qui ne devraient jamais être négatives. Mais aujourd’hui j’ai remarqué cette situation dans mon code:
void CreateRequestHeader( unsigned bitsAvailable, unsigned mandatoryDataSize, unsigned optionalDataSize ) { If ( bitsAvailable – mandatoryDataSize >= optionalDataSize ) { // Optional data fits, so add it to the header. } // BUG! The above includes the optional part even if // mandatoryDataSize > bitsAvailable. }
Dois-je commencer à utiliser int au lieu de unsigned int pour les nombres, même s’ils ne peuvent pas être négatifs?
Devrais-je toujours …
La réponse à “Dois-je toujours …” est certainement “non”, il y a beaucoup de facteurs qui déterminent si vous devez utiliser un type de données. La cohérence est importante.
Mais, il s’agit d’une question hautement subjective, il est vraiment facile de gâcher les non signés:
for (unsigned int i = 10; i >= 0; i--);
se traduit par une boucle infinie.
C’est pourquoi certains guides de style, notamment le Guide de style C ++ de Google, découragent les types de données unsigned
.
À mon avis, je n’ai pas rencontré beaucoup de bugs causés par ces problèmes de types de données non signés – je dirais que les assertions permettent de vérifier votre code et de les utiliser judicieusement (et moins lorsque vous effectuez des calculs).
Une chose qui n’a pas été mentionnée est que l’ échange de numéros signés / non signés peut conduire à des bogues de sécurité . Ceci est un gros problème, car de nombreuses fonctions de la bibliothèque C standard prennent / retournent des nombres non signés (fread, memcpy, malloc etc. prennent size_t
parameters size_t
)
Par exemple, prenez l’exemple inoffensif suivant (à partir du code réel):
//Copy a user-defined structure into a buffer and process it char* processNext(char* data, short length) { char buffer[512]; if (length <= 512) { memcpy(buffer, data, length); process(buffer); return data + length; } else { return -1; } }
Semble inoffensif, non? Le problème est que la length
est signée, mais est convertie en unsigned lorsqu’elle est transmise à memcpy
. Ainsi, définir une longueur sur SHRT_MIN
valide le test <= 512
, mais memcpy
à copier plus de 512 octets dans le tampon - cela permet à un attaquant de remplacer l'adresse de retour de la fonction et (après un peu de travail) ordinateur!
Vous pouvez naïvement dire: "Il est tellement évident que la longueur doit être size_t
ou vérifiée pour être >= 0
, je ne pourrais jamais faire cette erreur" . Sauf que je vous garantis que si vous avez déjà écrit quelque chose de non sortingvial, vous l'avez. Ainsi que les auteurs de Windows , Linux , BSD , Solaris , Firefox , OpenSSL , Safari , MS Paint , Internet Explorer , Google Picasa , Opera , Flash , Open Office , Subversion , Apache , Python , PHP , Pidgin , Gimp , ... sur et sans relâche ... - et ce sont tous des gens shinys dont le travail est de connaître la sécurité.
En bref, utilisez toujours size_t
pour les tailles.
Man, la programmation est difficile .
Dans certains cas, vous devez utiliser des types entiers non signés:
size_t
avec du code qui utilise des types non signés (par exemple, des routines de bibliothèque standard acceptant / size_t
valeurs size_t
. Mais pour l’arithmétique générale, le fait est que lorsque vous dites que quelque chose “ne peut pas être négatif”, cela ne signifie pas nécessairement que vous devez utiliser un type non signé. Parce que vous pouvez mettre une valeur négative dans un non signé, c’est juste que cela deviendra une valeur très importante lorsque vous allez le sortir. Donc, si vous voulez dire que les valeurs négatives sont interdites, comme pour une fonction racine carrée de base, alors vous énoncez une condition préalable de la fonction, et vous devriez l’affirmer. Et vous ne pouvez pas affirmer que ce qui ne peut pas être est; vous avez besoin d’un moyen de conserver les valeurs hors bande pour pouvoir les tester (il s’agit de la même logique derrière getchar()
renvoie un caractère int
et not char
).
De plus, le choix entre signature et signature peut avoir des répercussions pratiques sur les performances. Jetez un oeil au code (artificiel) ci-dessous:
#include bool foo_i(int a) { return (a + 69) > a; } bool foo_u(unsigned int a) { return (a + 69u) > a; }
Les deux foo
sont les mêmes sauf pour le type de leur paramètre. Mais, compilé avec c99 -fomit-frame-pointer -O2 -S
, vous obtenez:
.file "try.c" .texte .p2align 4, 15 .globl foo_i .type foo_i, @function foo_i: movl $ 1,% eax ret .size foo_i,.-foo_i .p2align 4, 15 .globl foo_u .type foo_u, @function foo_u: movl 4 (% esp),% eax leal 69 (% eax),% edx cmpl% eax,% edx seta% al ret .size foo_u,.-foo_u .ident "GCC: (Debian 4.4.4-7) 4.4.4" .section .note.GNU-stack, "", @ progbits
Vous pouvez voir que foo_i()
est plus efficace que foo_u()
. C’est parce que le débordement arithmétique non signé est défini par le standard pour “boucler”, donc (a + 69u)
peut très bien être plus petit que a
si a
est très grand, et donc il doit y avoir du code pour ce cas. Par contre, le débordement arithmétique signé n’est pas défini, donc GCC ira de l’avant et présumera que l’arithmétique signée ne déborde pas , et donc (a + 69)
ne peut jamais être inférieur à a
. Le choix de types non signés sans discrimination peut donc avoir un impact inutilement sur les performances.
Bjarne Stroustrup, créateur de C ++, met en garde contre l’utilisation de types non signés dans son livre Le langage de programmation C ++:
Les types entiers non signés sont idéaux pour les utilisations qui traitent le stockage comme un tableau de bits. Utiliser un non signé au lieu d’un int pour gagner encore un bit pour représenter des entiers positifs n’est presque jamais une bonne idée. Les tentatives visant à garantir que certaines valeurs sont positives en déclarant des variables non signées seront généralement annulées par les règles de conversion implicites.
La réponse est oui. Le type int “unsigned” de C et C ++ n’est pas un “nombre entier toujours positif”, quel que soit le nom du type. Le comportement des ints non signés C / C ++ n’a aucun sens si vous essayez de lire le type comme “non négatif” … par exemple:
En effet, les nombres non signés sont très utiles dans certains cas car ce sont des éléments de l’anneau “integers-modulo-N” avec N étant une puissance de deux. Ints non signés sont utiles lorsque vous souhaitez utiliser cette arithmétique modulo-n, ou en tant que bitmasks; ils ne sont PAS utiles en tant que quantités.
Malheureusement, en C et C ++, les non signés étaient également utilisés pour représenter des quantités non négatives afin de pouvoir utiliser tous les 16 bits lorsque les nombres entiers pouvant alors utiliser 32k ou 64k étaient considérés comme une grande différence. Je le classerais essentiellement comme un accident historique… vous ne devriez pas essayer de lire une logique car il n’y avait pas de logique.
Au fait, à mon avis, c’était une erreur … si 32k ne suffisait pas, alors 64k ne suffira pas non plus; abuser de l’entier modulo simplement à cause d’un bit supplémentaire à mon avis était un coût trop élevé à payer. Bien sûr, il aurait été raisonnable de faire si un type non négatif correct était présent ou défini … mais la sémantique non signée est tout simplement mauvaise pour l’utiliser comme non négatif.
Parfois, vous pouvez trouver qui dit que non signé est bon parce que cela “documente” que vous ne voulez que des valeurs non négatives … cependant, cette documentation n’a de valeur que pour les personnes qui ne savent pas vraiment comment fonctionne non signé pour C ou C ++. Pour moi, voir un type non signé utilisé pour des valeurs non négatives signifie simplement que l’auteur du code ne comprenait pas le langage de cette partie.
Si vous comprenez et souhaitez vraiment le comportement “wrapping” des ints non signés, alors ils sont le bon choix (par exemple, j’utilise presque toujours “unsigned char” lorsque je gère des octets); Si vous n’allez pas utiliser le comportement d’encapsulation (et que ce comportement va simplement vous poser problème dans le cas de la différence que vous avez montrée), cela indique clairement que le type non signé est un mauvais choix et que vous devrait coller avec les plaines.
Est-ce que cela signifie que le type de retour C ++ std::vector<>::size()
est un mauvais choix? Oui … c’est une erreur. Mais si vous dites, soyez prêt à être appelé de mauvais noms par qui ne comprend pas que le nom “unsigned” est juste un nom … ce qui compte, c’est le comportement et c’est un comportement “modulo-n” (et non on considérerait un type “modulo-n” pour la taille d’un conteneur un choix judicieux).
Je semble être en désaccord avec la plupart des gens ici, mais je trouve les types unsigned
très utiles, mais pas sous leur forme historique brute .
Si, par conséquent, vous vous en tenez à la sémantique que représente un type pour vous, il ne devrait y avoir aucun problème: utilisez size_t
(unsigned) pour les index de tableau, les offsets de données, etc. off_t
(signed) pour les offsets de fichiers. Utilisez ptrdiff_t
(signé) pour les différences de pointeurs. Utilisez uint8_t
pour les petits entiers non signés et int8_t
pour les entiers signés. Et vous évitez au moins 80% des problèmes de portabilité.
Et n’utilisez pas int
, long
, unsigned
, char
si vous ne devez pas. Ils appartiennent aux livres d’histoire. (Parfois, vous devez, les erreurs de retour, les champs de bits, par exemple)
Et pour revenir à votre exemple:
bitsAvailable – mandatoryDataSize >= optionalDataSize
peut être facilement réécrit comme
bitsAvailable >= optionalDataSize + mandatoryDataSize
ce qui n’évite pas le problème d’un débordement potentiel ( assert
c’est votre ami) mais je pense que vous vous rapprochez un peu de l’idée que vous voulez tester.
if (bitsAvailable >= optionalDataSize + mandatoryDataSize) { // Optional data fits, so add it to the header. }
Sans bogue, tant que obligatoireDataSize + optionalDataSize ne peut pas dépasser le type d’entier non signé – la dénomination de ces variables m’amène à penser que c’est probablement le cas.
Vous ne pouvez pas éviter complètement les types non signés dans le code portable, car de nombreux typedefs dans la bibliothèque standard sont non signés (notamment size_t
), et de nombreuses fonctions les renvoient (par exemple std::vector<>::size()
).
Cela dit, je préfère généralement m’en tenir aux types signés pour les raisons que vous avez décrites. Ce n’est pas seulement le cas que vous évoquez – en cas d’arithmétique signée / non signée, l’argument signé est discrètement promu en non signé.
D’après les commentaires sur l’un des articles du blog d’Eric Lipperts (voir ici ):
Jeffrey L. Whitledge
Une fois que j’ai développé un système dans lequel les valeurs négatives n’avaient aucun sens en tant que paramètre, plutôt que de valider que les valeurs des parameters étaient non négatives, je pensais que ce serait une bonne idée d’utiliser Uint à la place. J’ai rapidement découvert que chaque fois que j’utilisais ces valeurs pour quelque chose (comme appeler les méthodes BCL), elles étaient converties en entiers signés. Cela signifiait que je devais valider que les valeurs ne dépassaient pas la plage entière signée en haut, donc je n’ai rien gagné. De plus, chaque fois que le code était appelé, les ints utilisés (souvent reçus des fonctions BCL) devaient être convertis en uints. Il n’a pas fallu longtemps avant que je change toutes ces notes en ints et que je prenne toutes ces mesures inutiles. Je dois encore valider que les chiffres ne sont pas négatifs, mais le code est beaucoup plus propre!
Eric Lippert
Je n’aurais pas dit mieux moi même. Vous n’avez presque jamais besoin de la scope d’une uint, et ils ne sont pas compatibles CLS. La méthode standard pour représenter un petit entier est avec “int”, même s’il y a des valeurs qui sont hors limites. Une bonne règle de base: n’utilisez “uint” que dans les situations où vous interagissez avec du code non géré qui attend des uints, ou où l’entier en question est clairement utilisé comme un ensemble de bits, pas un nombre. Essayez toujours de l’éviter dans les interfaces publiques. – Eric
La situation où (bitsAvailable – mandatoryDataSize)
produit un résultat “inattendu” lorsque les types sont unsigned et bitsAvailable < mandatoryDataSize
est une raison pour laquelle des types parfois signés sont parfois utilisés même si les données ne sont jamais censées être négatives.
Je pense qu’il n’ya pas de règle absolue - j’utilise généralement des types non signés pour des données qui n’ont aucune raison d’être négatives, mais il faut s’assurer que l’encapsulation arithmétique n’expose pas de bogues.
Ensuite, si vous utilisez des types signés, vous devez parfois considérer le dépassement de capacité:
MAX_INT + 1
La clé est que vous devez faire attention lors de l'exécution de l'arithmétique pour ces types de bogues.
Non, vous devez utiliser le type qui convient à votre application. Il n’y a pas de règle d’or. Parfois, sur de petits microcontrôleurs, il est par exemple plus rapide et efficace d’utiliser de la mémoire, dans la mesure du possible, comme variables natives, mais c’est un cas très particulier. Je recommande également d’utiliser stdint.h dans la mesure du possible. Si vous utilisez Visual Studio, vous pouvez trouver des versions sous licence BSD.
S’il y a une possibilité de débordement, affectez les valeurs au type de données suivant lors du calcul, à savoir:
void CreateRequestHeader( unsigned int bitsAvailable, unsigned int mandatoryDataSize, unsigned int optionalDataSize ) { signed __int64 available = bitsAvailable; signed __int64 mandatory = mandatoryDataSize; signed __int64 optional = optionalDataSize; if ( (mandatory + optional) <= available ) { // Optional data fits, so add it to the header. } }
Sinon, vérifiez simplement les valeurs individuellement au lieu de calculer:
void CreateRequestHeader( unsigned int bitsAvailable, unsigned int mandatoryDataSize, unsigned int optionalDataSize ) { if ( bitsAvailable < mandatoryDataSize ) { return; } bitsAvailable -= mandatoryDataSize; if ( bitsAvailable < optionalDataSize ) { return; } bitsAvailable -= optionalDataSize; // Optional data fits, so add it to the header. }
Vous devrez examiner les résultats des opérations que vous effectuez sur les variables pour vérifier si vous pouvez dépasser / sous-traiter – dans votre cas, le résultat est potentiellement négatif. Dans ce cas, il vaut mieux utiliser les équivalents signés.
Je ne sais pas si c’est possible dans c, mais dans ce cas, je voudrais simplement lancer la chose XY dans un int.
Si vos nombres ne doivent jamais être inférieurs à zéro, mais ont une chance d’être <0, utilisez tous des entiers signés et saupoudrez des assertions ou d'autres vérifications à l'exécution. Si vous travaillez en réalité avec des valeurs 32 bits (ou 64 ou 16, selon votre architecture cible) où le bit le plus significatif signifie autre chose que "-", vous ne devez utiliser que des variables non signées pour les conserver. Il est plus facile de détecter les dépassements d'entiers où un nombre qui devrait toujours être positif est très négatif que lorsqu'il est nul, donc si vous n'avez pas besoin de ce bit, allez avec les signés.
Supposons que vous ayez besoin de compter de 1 à 50000. Vous pouvez le faire avec un entier non signé de deux octets, mais pas avec un entier signé de deux octets (si l’espace compte beaucoup).