size_t ou int pour les dimensions, l'index, etc.

15

En C ++, size_t(ou, plus correctement, T::size_typequi est "habituellement" size_t; c'est-à-dire un unsignedtype) est utilisé comme valeur de retour pour size(), l'argument de operator[], etc. (voir std::vector, et. Al.)

D'un autre côté, les langages .NET utilisent int(et, éventuellement, long) dans le même but; en fait, les langages compatibles CLS ne sont pas requis pour prendre en charge les types non signés .

Étant donné que .NET est plus récent que C ++, quelque chose me dit qu'il peut y avoir des problèmes d' utilisation unsigned intmême pour des choses qui "ne peuvent pas" être négatives comme un index ou une longueur de tableau. L'approche C ++ est-elle un «artefact historique» pour la compatibilité descendante? Ou existe-t-il des compromis de conception réels et importants entre les deux approches?

Pourquoi est-ce important? Eh bien ... que dois-je utiliser pour une nouvelle classe multidimensionnelle en C ++; size_tou int?

struct Foo final // e.g., image, matrix, etc.
{
    typedef int32_t /* or int64_t*/ dimension_type; // *OR* always "size_t" ?
    typedef size_t size_type; // c.f., std::vector<>

    dimension_type bar_; // maybe rows, or x
    dimension_type baz_; // e.g., columns, or y

    size_type size() const { ... } // STL-like interface
};
Ðаn
la source
6
À noter: à plusieurs endroits dans le .NET Framework, -1est renvoyé par les fonctions qui renvoient un index, pour indiquer «introuvable» ou «hors de portée». Il est également revenu des Compare()fonctions (implémentation IComparable). Un entier 32 bits est considéré comme le type de frappe pour un nombre général, pour ce que j'espère être des raisons évidentes.
Robert Harvey

Réponses:

9

Étant donné que .NET est plus récent que C ++, quelque chose me dit qu'il peut y avoir des problèmes à utiliser int non signé même pour des choses qui "ne peuvent pas" être négatives comme un index de tableau ou une longueur.

Oui. Pour certains types d'applications comme le traitement d'images ou le traitement de tableaux, il est souvent nécessaire d'accéder à des éléments relatifs à la position actuelle:

sum = data[k - 2] + data[k - 1] + data[k] + data[k + 1] + ...

Dans ces types d'applications, vous ne pouvez pas effectuer de vérification de plage avec des entiers non signés sans réfléchir soigneusement:

if (k - 2 < 0) {
    throw std::out_of_range("will never be thrown"); 
}

if (k < 2) {
    throw std::out_of_range("will be thrown"); 
}

if (k < 2uL) {
    throw std::out_of_range("will be thrown, without signedness ambiguity"); 
}

Au lieu de cela, vous devez réorganiser votre expression de vérification de plage. Voilà la principale différence. Les programmeurs doivent également se souvenir des règles de conversion des nombres entiers. En cas de doute, relisez http://en.cppreference.com/w/cpp/language/operator_arithmetic#Conversions

De nombreuses applications n'ont pas besoin d'utiliser de très grands indices de tableau, mais elles doivent effectuer des vérifications de plage. De plus, beaucoup de programmeurs ne sont pas formés pour faire cette gymnastique de réarrangement d'expression. Une seule occasion manquée ouvre la porte à un exploit.

C # est en effet conçu pour les applications qui n'auront pas besoin de plus de 2 ^ 31 éléments par tableau. Par exemple, une application de feuille de calcul n'a pas besoin de traiter autant de lignes, de colonnes ou de cellules. C # gère la limite supérieure en ayant une arithmétique vérifiée facultative qui peut être activée pour un bloc de code avec un mot-clé sans jouer avec les options du compilateur. Pour cette raison, C # favorise l'utilisation d'un entier signé. Lorsque ces décisions sont considérées dans leur ensemble, cela fait sens.

C ++ est tout simplement différent et il est plus difficile d'obtenir du code correct.

En ce qui concerne l'importance pratique de permettre à l'arithmétique signée de supprimer une violation potentielle du "principe du moindre étonnement", un exemple en est OpenCV, qui utilise un entier signé 32 bits pour l'index des éléments de la matrice, la taille du tableau, le nombre de canaux de pixels, etc. Image le traitement est un exemple de domaine de programmation qui utilise fortement un index de tableau relatif. Le dépassement d'entier non signé (résultat négatif enveloppé autour) compliquera gravement la mise en œuvre de l'algorithme.

rwong
la source
C'est exactement ma situation; merci pour les exemples spécifiques. (Oui, je le sais, mais il peut être utile d'avoir des «autorités supérieures» pour citer.)
Le
1
@Dan: si vous avez besoin de citer quelque chose, ce message serait mieux.
rwong
1
@Dan: John Regehr recherche activement ce problème dans les langages de programmation. Voir blog.regehr.org/archives/1401
rwong
Il y a des opinions contraires: gustedt.wordpress.com/2013/07/15/…
rwong
14

Cette réponse dépend vraiment de qui va utiliser votre code et des normes qu'ils souhaitent voir.

size_t est une taille entière avec un objectif:

Le type size_test un type entier non signé défini par l'implémentation qui est suffisamment grand pour contenir la taille en octets de n'importe quel objet. (Spécification C ++ 11 18.2.6)

Ainsi, chaque fois que vous souhaitez travailler avec la taille des objets en octets, vous doit utiliser size_t. Maintenant, dans de nombreux cas, vous n'utilisez pas ces dimensions / index pour compter les octets, mais la plupart des développeurs choisissent de les utiliser à des fins size_tde cohérence.

Notez que vous devez toujours utiliser size_tsi votre classe est conçue pour avoir l'apparence d'une classe STL. Toutes les classes STL de la spécification utilisent size_t. Il est valide pour que le compilateur typedef size_tsoit unsigned int, et il est également valide pour qu'il soit typé à unsigned long. Si vous utilisez intou longdirectement, vous finirez par rencontrer des compilateurs où une personne qui pense que votre classe a suivi le style de la STL est piégée parce que vous n'avez pas suivi la norme.

Quant à l'utilisation de types signés, il y a quelques avantages:

  • Noms plus courts - il est vraiment facile pour les gens de taper int, mais beaucoup plus difficile à encombrer le code unsigned int.
  • Un entier pour chaque taille - Il n'y a qu'un seul entier conforme CLS de 32 bits, qui est Int32. En C ++, il y a deux ( int32_tet uint32_t). Cela peut simplifier l'interopérabilité des API

Le gros inconvénient des types signés est évident: vous perdez la moitié de votre domaine. Un numéro signé ne peut pas compter autant qu'un numéro non signé. Lorsque C / C ++ est apparu, c'était très important. L'un devait être en mesure de gérer toutes les capacités du processeur, et pour ce faire, vous deviez utiliser des numéros non signés.

Pour les types d'applications .NET ciblés, le besoin d'un index non signé de domaine complet n'était pas aussi fort. De nombreux objectifs pour de tels nombres ne sont tout simplement pas valides dans un langage géré (le regroupement de mémoire vient à l'esprit). De plus, avec la sortie de .NET, les ordinateurs 64 bits étaient clairement l'avenir. Nous sommes loin d'avoir besoin de la gamme complète d'un entier 64 bits, donc sacrifier un bit n'est pas aussi douloureux qu'auparavant. Si vous avez vraiment besoin de 4 milliards d'index, vous passez simplement à l'utilisation d'entiers 64 bits. Au pire, vous l'exécutez sur une machine 32 bits et c'est un peu lent.

Je considère le métier comme un métier de commodité. S'il vous arrive d'avoir une puissance de calcul suffisante pour que cela ne vous dérange pas de gaspiller un peu de votre type d'index que vous n'utiliserez jamais, alors il est pratique de simplement taper intou longet de s'en éloigner. Si vous trouvez que vous vouliez vraiment ce dernier morceau, alors vous auriez probablement dû faire attention à la signature de vos numéros.

Cort Ammon - Rétablir Monica
la source
disons que la mise en œuvre de size()was return bar_ * baz_;; cela ne crée-t-il pas maintenant un problème potentiel de débordement d'entier (bouclage) que je n'aurais pas si je n'utilisais pas size_t?
Ðаn
5
@Dan Vous pouvez créer des cas comme celui-là où des entrées non signées importent, et dans ces cas, il est préférable d'utiliser les fonctionnalités complètes du langage pour le résoudre. Cependant, je dois dire que ce serait une construction intéressante d'avoir une classe où bar_ * baz_peut déborder un entier signé mais pas un entier non signé. En nous limitant au C ++, il convient de noter que le débordement non signé est défini dans la spécification, mais le débordement signé est un comportement indéfini, donc si l'arithmétique modulo des entiers non signés est souhaitable, utilisez-les certainement, car elle est réellement définie!
Cort Ammon - Rétablir Monica
1
@Dan - si le size()débordement de la multiplication signée , vous êtes dans la langue UB land. (et en fwrapvmode, voir suivant :) Quand alors , avec juste un tout petit peu plus, il a débordé la multiplication non signée , vous en terre de bug de code utilisateur - vous renverriez une taille fausse. Je ne pense donc pas que les achats non signés achètent beaucoup ici.
Martin Ba
4

Je pense que la réponse de rwong ci-dessus met déjà très bien en évidence les problèmes.

J'ajouterai mon 002:

  • size_tc'est-à-dire une taille qui ...

    peut stocker la taille maximale d'un objet théoriquement possible de tout type (y compris un tableau).

    ... n'est requis que pour les indices de plage lorsque sizeof(type)==1, c'est-à-dire si vous avez affaire à des chartypes byte ( ). (Mais, notons-le, il peut être plus petit qu'un type ptr :

  • En tant que tel, xxx::size_typepourrait être utilisé dans 99,9% des cas, même s'il s'agissait d'un type de taille signée. (comparer ssize_t)
  • Le fait que std::vectorand friends ait choisi size_tun type non signé pour la taille et l'indexation est considéré par certains comme un défaut de conception. Je suis d'accord. (Sérieusement, prenez 5 minutes et regardez la foudre parler CppCon 2016: Jon Kalb "non signé: une ligne directrice pour un meilleur code" .)
  • Lorsque vous concevez une API C ++ aujourd'hui, vous êtes dans une situation difficile: utilisez size_tpour être cohérent avec la bibliothèque standard, ou utilisez (une signature ) intptr_tou ssize_tpour des calculs d'indexation faciles et moins sujets aux bogues.
  • N'utilisez pas int32 ou int64 - utilisez intptr_tsi vous voulez aller signé et que vous voulez la taille du mot machine, ou utilisez ssize_t.

Pour répondre directement à la question, il ne s'agit pas entièrement d'un "artefact historique", car la question théorique de la nécessité de traiter plus de la moitié de ("l'indexation" ou) de l'espace d'adressage doit être, aehm, traitée d'une manière ou d'une autre dans un langage de bas niveau comme C ++.

Avec le recul, je, personnellement , pense, il est un défaut de conception que la bibliothèque standard utilise non signé size_tdans le lieu même où elle ne représente pas une taille de mémoire brute, mais une capacité de données typées, comme pour les collections:

  • étant donné les règles de promotion des entiers C ++ ->
  • les types non signés ne font tout simplement pas de bons candidats pour les types "sémantiques" pour quelque chose comme une taille qui n'est pas sémantiquement signée.

Je vais répéter le conseil de Jon ici:

  • Sélectionnez les types pour les opérations qu'ils prennent en charge (pas la plage de valeurs). (*1)
  • N'utilisez pas de types non signés dans votre API. Cela cache des bogues sans aucun avantage à la hausse.
  • N'utilisez pas "non signé" pour les quantités. (* 2)

(* 1) c'est-à-dire unsigned == bitmask, ne faites jamais de calcul dessus (ici frappe la première exception - vous pouvez avoir besoin d'un compteur qui encapsule - ce doit être un type non signé.)

(* 2) quantités signifiant quelque chose sur lequel vous comptez et / ou faites des calculs.

Martin Ba
la source
Que voulez-vous dire par "mémoire plate entièrement disponible"? De plus, bien sûr, vous ne voulez pas ssize_t, défini comme le pendentif signé au size_tlieu de intptr_t, qui peut stocker n'importe quel pointeur (non membre) et pourrait donc être plus grand?
Déduplicateur
@Deduplicator - Eh bien, je suppose que j'ai peut-être mal compris la size_tdéfinition. Voir size_t vs intptr et en.cppreference.com/w/cpp/types/size_t J'ai appris quelque chose de nouveau aujourd'hui. :-) Je pense que le reste des arguments tient, je vais voir si je peux réparer les types utilisés.
Martin Ba
0

J'ajouterai simplement que pour des raisons de performances, j'utilise normalement size_t, pour garantir que les erreurs de calcul provoquent un dépassement de capacité, ce qui signifie que les deux vérifications de plage (en dessous de zéro et au-dessus de size ()) peuvent être réduites à un:

en utilisant int signé:

int32_t i = GetRandomNumberFromRange(-1000, 1000);

if (i < 0)
{
    //error
}

if (i > size())
{
    //error
}

en utilisant un entier non signé:

int32_t i = GetRandomNumberFromRange(-1000, 1000);

/// This will underflow any number below zero, so that it becomes a very big *positive* number instead.
uint32_t asUnsigned = static_cast<uint32_t>(i);

/// We now don't need to check for below zero, since an unsigned integer can only be positive.
if (asUnsigned > size())
{
    //error
}
asger
la source
1
Vous voulez vraiment l' expliquer plus en détail.
Martin Ba
Pour rendre la réponse plus utile, vous pouvez peut-être décrire à quoi ressemblent les limites du tableau d'entiers ou la comparaison de décalage (signé et non signé) dans le code machine de divers fournisseurs de compilateurs. Il existe de nombreux compilateurs C ++ en ligne et sites de désassemblage qui peuvent afficher le code machine compilé correspondant pour le code C ++ et les indicateurs de compilateur donnés.
rwong
J'ai essayé d'expliquer cela un peu plus.
asger