Utilisation d'entiers non signés en C et C ++

23

J'ai une question très simple qui me déroute depuis longtemps. Je traite avec des réseaux et des bases de données, donc beaucoup de données que je traite sont des compteurs 32 bits et 64 bits (non signés), des identifiants d'identification 32 bits et 64 bits (également sans mappage significatif pour le signe). Je ne suis pratiquement jamais confronté à une véritable question verbale qui pourrait être exprimée en nombre négatif.

Moi et mes collègues utilisons régulièrement des types non signés comme uint32_tet uint64_tpour ces questions et parce que cela arrive si souvent, nous les utilisons également pour les index de tableaux et d'autres utilisations d'entiers courantes.

En même temps, divers guides de codage que je lis (par exemple Google) découragent l'utilisation de types entiers non signés, et pour autant que je sache, ni Java ni Scala n'ont de types entiers non signés.

Donc, je ne pouvais pas comprendre quelle était la bonne chose à faire: utiliser des valeurs signées dans notre environnement serait très gênant, tout en codant les guides pour insister pour faire exactement cela.

zzz777
la source

Réponses:

31

Il y a deux écoles de pensée à ce sujet, et aucune ne sera jamais d'accord.

Le premier fait valoir qu'il existe certains concepts qui sont intrinsèquement non signés - tels que les index de tableau. Cela n'a aucun sens d'utiliser des numéros signés pour ceux-ci car cela peut entraîner des erreurs. Il peut également imposer des limites inutiles aux choses - un tableau qui utilise des index 32 bits signés ne peut accéder qu'à 2 milliards d'entrées, tandis que le passage à des numéros 32 bits non signés autorise 4 milliards d'entrées.

La seconde fait valoir que dans tout programme utilisant des nombres non signés, vous finirez tôt ou tard par faire de l'arithmétique mixte signée-non signée. Cela peut donner des résultats étranges et inattendus: la conversion d'une grande valeur non signée en signé donne un nombre négatif et inversement la conversion d'un nombre négatif en non signé en donne un grand positif. Cela peut être une grande source d'erreurs.

Simon B
la source
8
Des problèmes arithmétiques mixtes signés-non signés sont détectés par le compilateur; Gardez simplement votre build sans avertissement (avec un niveau d'avertissement suffisamment élevé). En outre, intest plus court pour taper :)
rucamzu
7
Confession: je suis avec la deuxième école de pensée, et bien que je comprenne les considérations pour les types non signés: intest plus que suffisant pour les index de tableau 99,99% des fois. Les problèmes arithmétiques signés - non signés sont beaucoup plus courants et ont donc priorité sur ce qu'il faut éviter. Oui, les compilateurs vous en avertissent, mais combien d'avertissements recevez-vous lors de la compilation d'un projet de taille importante? Ignorer les avertissements est dangereux et une mauvaise pratique, mais dans le monde réel ...
Elias Van Ootegem
11
+1 à la réponse. Attention : Opinions contondantes à venir: 1: Ma réponse à la deuxième école de pensée est la suivante: je parierais que quiconque obtient des résultats inattendus de types intégraux non signés en C aura un comportement indéfini (et non pas du type purement académique) dans leurs programmes C non triviaux qui utilisent des types intégraux signés . Si vous ne connaissez pas suffisamment C pour penser que les types non signés sont les meilleurs à utiliser, je vous conseille d'éviter C. 2: Il y a exactement un type correct pour les index et les tailles de tableau en C, et c'est size_t, sauf s'il y a un cas spécial bonne raison sinon.
mtraceur
5
Vous rencontrez des problèmes sans signature mixte. Calculez simplement un entier non signé moins un entier non signé.
gnasher729
4
Ne vous opposant pas à Simon, seulement à la première école de pensée qui soutient qu '"il existe certains concepts qui sont intrinsèquement non signés - tels que les index de tableaux". spécifiquement: "Il y a exactement un type correct pour les index de tableaux ... en C", Bullshit! . Nous DSPers utilisons des indices négatifs tout le temps. en particulier avec des réponses impulsionnelles de symétrie paire ou impaire qui ne sont pas causales. et pour les mathématiques LUT. Je suis dans la deuxième école de pensée, mais je pense qu'il est utile d'avoir les deux entiers signés et non signés en C et C ++.
robert bristow-johnson
21

Tout d'abord, la directive de codage de Google C ++ n'est pas très bonne à suivre: elle évite les choses comme les exceptions, le boost, etc. qui sont des agrafes du C ++ moderne. Deuxièmement, ce n'est pas parce qu'une certaine directive fonctionne pour l'entreprise X qu'elle vous conviendra. Je continuerais à utiliser des types non signés, car vous en avez un bon besoin.

Une règle d'or décente pour C ++ est: préférez, intsauf si vous avez une bonne raison d'utiliser autre chose.

bstamour
la source
8
Ce n'est pas du tout ce que je veux dire. Les constructeurs servent à établir des invariants, et comme ce ne sont pas des fonctions, ils ne peuvent pas simplement return falsesi cet invariant n'est pas établi. Ainsi, vous pouvez soit séparer les choses et utiliser les fonctions init pour vos objets, soit lancer un std::runtime_error, laisser le déroulement de la pile se produire et laisser tous vos objets RAII se nettoyer automatiquement et vous, le développeur, pouvez gérer l'exception là où cela est pratique pour vous de le faire.
bstamour
5
Je ne vois pas en quoi le type d'application fait la différence. Chaque fois que vous appelez un constructeur sur un objet, vous établissez un invariant avec les paramètres. Si cet invariant ne peut pas être respecté, vous devez signaler une erreur sinon votre programme n'est pas en bon état. Étant donné que les constructeurs ne peuvent pas renvoyer un indicateur, lever une exception est une option naturelle. Veuillez expliquer clairement pourquoi une application métier ne bénéficierait pas d'un tel style de codage.
bstamour
8
Je doute fortement que la moitié de tous les programmeurs C ++ soient incapables d'utiliser correctement les exceptions. Mais de toute façon, si vous pensez que vos collègues sont incapables d'écrire du C ++ moderne, restez loin du C ++ moderne.
bstamour
6
@ zzz777 N'utilisez pas d'exceptions? Ont des constructeurs privés qui sont enveloppés par des fonctions d'usine publiques qui interceptent les exceptions et font quoi - retourner un nullptr? retourner un objet "par défaut" (quoi que cela puisse signifier)? Vous n'avez rien résolu - vous venez de cacher le problème sous un tapis, et j'espère que personne ne le découvrira.
Mael
5
@ zzz777 Si vous allez quand même planter la boîte, pourquoi vous en souciez-vous si cela se produit à partir d'une exception ou signal(6)? Si vous utilisez une exception, les 50% de développeurs qui savent comment les gérer peuvent écrire du bon code, et le reste peut être porté par leurs pairs.
IllusiveBrian
6

Les autres réponses manquent d'exemples réels, je vais donc en ajouter un. L'une des raisons pour lesquelles j'essaie (personnellement) d'éviter les types non signés.

Pensez à utiliser size_t standard comme index de tableau:

for (size_t i = 0; i < n; ++i)
    // do something here;

Ok, parfaitement normal. Ensuite, considérons que nous avons décidé de changer la direction de la boucle pour une raison quelconque:

for (size_t i = n - 1; i >= 0; --i)
    // do something here;

Et maintenant ça ne marche pas. Si nous l'utilisions intcomme itérateur, il n'y aurait aucun problème. J'ai vu une telle erreur deux fois au cours des deux dernières années. Une fois arrivé en production et difficile à déboguer.

Une autre raison pour moi est les avertissements ennuyeux, qui vous font écrire quelque chose comme ça à chaque fois :

int n = 123;  // for some reason n is signed
...
for (size_t i = 0; i < size_t(n); ++i)

Ce sont des choses mineures, mais elles s'additionnent. J'ai l'impression que le code est plus propre si seuls des entiers signés sont utilisés partout.

Edit: Bien sûr, les exemples semblent stupides, mais j'ai vu des gens faire cette erreur. S'il existe un moyen si simple de l'éviter, pourquoi ne pas l'utiliser?

Lorsque je compile le morceau de code suivant avec VS2015 ou GCC, je ne vois aucun avertissement avec des paramètres d'avertissement par défaut (même avec -Wall pour GCC). Vous devez demander -Wextra pour obtenir un avertissement à ce sujet dans GCC. C'est l'une des raisons pour lesquelles vous devez toujours compiler avec Wall et Wextra (et utiliser un analyseur statique), mais dans de nombreux projets réels, les gens ne le font pas.

#include <vector>
#include <iostream>


void unsignedTest()
{
    std::vector<int> v{ 1, 2 };

    for (int i = v.size() - 1; i >= 0; --i)
        std::cout << v[i] << std::endl;

    for (size_t i = v.size() - 1; i >= 0; --i)
        std::cout << v[i] << std::endl;
}

int main()
{
    unsignedTest();
    return 0;
}
Aleksei Petrenko
la source
Vous pouvez vous tromper encore plus avec les types signés ... Et votre exemple de code est si stupéfiant et manifestement faux que n'importe quel compilateur décent avertira si vous demandez des avertissements.
Déduplicateur
1
Dans le passé, j'ai eu recours à des horreurs telles que for (size_t i = n - 1; i < n; --i)cela fonctionnait bien.
Simon B
2
En parlant de boucles pour avec size_ten sens inverse, il existe une directive de codage dans le style defor (size_t revind = 0u; revind < n; ++revind) { size_t ind = n - 1u - revind; func(ind); }
rwong
2
@rwong Omg, c'est moche. Pourquoi ne pas simplement utiliser int? :)
Aleksei Petrenko
1
@AlexeyPetrenko - notez que ni les normes C ni C ++ actuelles ne garantissent une inttaille suffisante pour contenir toutes les valeurs valides de size_t. En particulier, intpeut autoriser les nombres uniquement jusqu'à 2 ^ 15-1, et le fait généralement sur les systèmes qui ont des limites d'allocation de mémoire de 2 ^ 16 (ou dans certains cas encore plus). longpeut être un pari plus sûr, bien qu'il ne soit toujours pas garanti de fonctionner. Seul size_test garanti de fonctionner sur toutes les plateformes et dans tous les cas.
Jules
4
for (size_t i = v.size() - 1; i >= 0; --i)
   std::cout << v[i] << std::endl;

Le problème ici est que vous avez écrit la boucle de manière onclever conduisant au comportement erroné. La construction de la boucle est comme si les débutants l'enseignaient pour les types signés (ce qui est correct et correct), mais elle ne convient tout simplement pas aux valeurs non signées. Mais cela ne peut pas servir de contre-argument contre l'utilisation de types non signés, la tâche ici est simplement de bien faire votre boucle. Et cela peut facilement être corrigé pour fonctionner de manière fiable pour les types non signés comme ceci:

for (size_t i = v.size(); i-- > 0; )
    std::cout << v[i] << std::endl;

Ce changement inverse simplement la séquence de la comparaison et de l'opération de décrémentation et est à mon avis le moyen le plus efficace, le moins perturbant, le plus propre et le plus court pour gérer les compteurs non signés dans les boucles en arrière. Vous feriez la même chose (intuitivement) lorsque vous utilisez une boucle while:

size_t i = v.size();
while (i > 0)
{
    --i;
    std::cout << v[i] << std::endl;
}

Aucun débordement ne peut se produire, le cas d'un conteneur vide est couvert implicitement, comme dans la variante bien connue de la boucle de compteur signée, et le corps de la boucle peut rester inchangé par rapport à un compteur signé ou une boucle directe. Il vous suffit de vous habituer à la construction de boucle d'aspect quelque peu étrange au début. Mais après avoir vu cela une douzaine de fois, il n'y a plus rien d'intelligible.

Je serais chanceux si les cours pour débutants montraient non seulement la bonne boucle pour les types signés mais aussi pour les types non signés. Cela éviterait quelques erreurs qui devraient à mon humble avis être imputées aux développeurs involontaires au lieu de blâmer le type non signé.

HTH

Don Pedro
la source
1

Les entiers non signés sont là pour une raison.

Considérez, par exemple, le transfert de données sous forme d'octets individuels, par exemple dans un paquet réseau ou un tampon de fichiers. Vous pouvez parfois rencontrer des bêtes telles que des entiers 24 bits. Décalage binaire aisé de trois entiers non signés 8 bits, pas si facile avec des entiers signés 8 bits.

Ou pensez aux algorithmes utilisant des tables de recherche de caractères. Si un caractère est un entier non signé 8 bits, vous pouvez indexer une table de recherche par une valeur de caractère. Cependant, que faites-vous si le langage de programmation ne prend pas en charge les entiers non signés? Vous auriez des index négatifs sur un tableau. Eh bien, je suppose que vous pourriez utiliser quelque chose comme charval + 128ça, mais c'est tout simplement moche.

En fait, de nombreux formats de fichiers utilisent des entiers non signés et si le langage de programmation d'application ne prend pas en charge les entiers non signés, cela pourrait être un problème.

Considérez ensuite les numéros de séquence TCP. Si vous écrivez un code de traitement TCP, vous voudrez certainement utiliser des entiers non signés.

Parfois, l'efficacité est tellement importante que vous avez vraiment besoin de ce bit supplémentaire d'entiers non signés. Prenons par exemple les appareils IoT qui sont expédiés par millions. De nombreuses ressources de programmation peuvent alors être justifiées pour être dépensées en micro-optimisations.

Je dirais que la justification pour éviter d'utiliser des types entiers non signés (arithmétique de signe mixte, comparaisons de signe mixte) peut être surmontée par un compilateur avec des avertissements appropriés. De tels avertissements ne sont généralement pas activés par défaut, mais voir par exemple -Wextraou séparément -Wsign-compare(activé automatiquement en C par -Wextra, bien que je ne pense pas qu'il soit activé automatiquement en C ++) et -Wsign-conversion.

Néanmoins, en cas de doute, utilisez un type signé. Plusieurs fois, c'est un choix qui fonctionne bien. Et activez ces avertissements du compilateur!

juhist
la source
0

Il existe de nombreux cas où les entiers ne représentent pas réellement des nombres, mais par exemple un masque de bits, un identifiant, etc. Fondamentalement, des cas où l'ajout de 1 à un entier n'a aucun résultat significatif. Dans ces cas, utilisez non signé.

Il existe de nombreux cas où vous faites de l'arithmétique avec des nombres entiers. Dans ces cas, utilisez des entiers signés pour éviter tout comportement incorrect autour de zéro. Voir de nombreux exemples de boucles, où l'exécution d'une boucle jusqu'à zéro utilise du code très intuitif ou est interrompue en raison de l'utilisation de nombres non signés. Il y a l'argument "mais les indices ne sont jamais négatifs" - bien sûr, mais les différences d'indices par exemple sont négatives.

Dans le cas très rare où les indices dépassent 2 ^ 31 mais pas 2 ^ 32, vous n'utilisez pas d'entiers non signés, vous utilisez des entiers 64 bits.

Enfin, un piège sympa: dans une boucle "pour (i = 0; i <n; ++ i) a [i] ..." si i n'est pas signé 32 bits et que la mémoire dépasse les adresses 32 bits, le compilateur ne peut pas optimiser l'accès à un [i] en incrémentant un pointeur, car à i = 2 ^ 32 - 1, je m'enroule. Même quand n ne devient jamais aussi gros. L'utilisation d'entiers signés évite cela.

gnasher729
la source
-5

Enfin, j'ai trouvé une très bonne réponse ici: "Secure Programming Cookbook" par J.Viega et M.Messier ( http://shop.oreilly.com/product/9780596003944.do )

Problèmes de sécurité avec des entiers signés:

  1. Si la fonction nécessite un paramètre positif, il est facile d'oublier de vérifier la plage inférieure.
  2. Modèle de bits non intuitif provenant de conversions de taille entière négatives.
  3. Modèle de bits non intuitif produit par l'opération de décalage vers la droite d'un entier négatif.

Il y a des problèmes avec les conversions signées <-> non signées, il est donc déconseillé d'utiliser le mixage.

zzz777
la source
1
Pourquoi est-ce une bonne réponse? Qu'est-ce que la recette 3.5? Que dit-il sur le débordement d'entier, etc.?
Baldrickk
Dans mon expérience pratique, c'est un très bon livre avec de précieux conseils tous les autres dans les aspects que j'ai essayés et il est assez ferme dans cette recommandation. Par rapport à cela, les dangers de débordements d'entiers sur des baies plus longues que la 4G semblent assez faibles. Si je dois gérer des tableaux aussi gros, mon programme aura beaucoup de réglages pour éviter les pénalités de performances.
zzz777
1
il ne s'agit pas de savoir si le livre est bon. Votre réponse ne fournit aucune justification pour l'utilisation de la recette, et tout le monde n'aura pas une copie du livre pour le rechercher. Regardez les exemples d'écriture d'une bonne réponse
Baldrickk
FYI vient d'apprendre une autre raison d'utiliser des entiers non signés: on peut facilement détecter un débordement: youtube.com/…
zzz777