size_t contre uintptr_t

246

La norme C garantit qu'il size_ts'agit d'un type pouvant contenir n'importe quel index de tableau. Cela signifie que, logiquement, size_tdevrait pouvoir contenir n'importe quel type de pointeur. J'ai lu sur certains sites que j'ai trouvés sur Google que cela est légal et / ou devrait toujours fonctionner:

void *v = malloc(10);
size_t s = (size_t) v;

Ainsi, en C99, la norme a introduit les types intptr_tet uintptr_t, qui sont des types signés et non signés garantis pour pouvoir contenir des pointeurs:

uintptr_t p = (size_t) v;

Alors, quelle est la différence entre utiliser size_tet uintptr_t? Les deux ne sont pas signés, et les deux devraient pouvoir contenir n'importe quel type de pointeur, de sorte qu'ils semblent fonctionnellement identiques. Y a-t-il une vraie raison impérieuse d'utiliser uintptr_t(ou mieux encore, a void *) plutôt que a size_t, autre que la clarté? Dans une structure opaque, où le champ ne sera géré que par des fonctions internes, y a-t-il une raison de ne pas le faire?

De même, ptrdiff_ta-t-il été un type signé capable de contenir des différences de pointeurs, et donc capable de contenir la plupart des pointeurs, alors en quoi est-il distinct intptr_t?

Tous ces types ne servent-ils pas fondamentalement des versions trivialement différentes de la même fonction? Sinon, pourquoi? Qu'est-ce que je ne peux pas faire avec l'un d'eux que je ne peux pas faire avec un autre? Si oui, pourquoi C99 a-t-il ajouté deux types essentiellement superflus au langage?

Je suis prêt à ignorer les pointeurs de fonction, car ils ne s'appliquent pas au problème actuel, mais n'hésitez pas à les mentionner, car j'ai une suspicion sournoise, ils seront au cœur de la "bonne" réponse.

Chris Lutz
la source

Réponses:

236

size_test un type qui peut contenir n'importe quel index de tableau. Cela signifie que, logiquement, size_t devrait pouvoir contenir n'importe quel type de pointeur

Pas nécessairement! Revenons aux jours des architectures segmentées 16 bits par exemple: un tableau peut être limité à un seul segment (donc un 16 bits size_tferait) MAIS vous pourriez avoir plusieurs segments (donc un type 32 bits intptr_tserait nécessaire pour choisir segment ainsi que son décalage). Je sais que ces choses semblent étranges en ces jours d'architectures non segmentées uniformément adressables, mais la norme DOIT répondre à une plus grande variété que "ce qui est normal en 2009", vous savez! -)

Alex Martelli
la source
6
Ceci, ainsi que les nombreux autres qui ont sauté à la même conclusion, explique la différence entre size_tet uintptr_tmais qu'en est-il ptrdiff_tet intptr_t- les deux ne pourraient-ils pas stocker la même plage de valeurs sur presque n'importe quelle plate-forme? Pourquoi avoir des types entiers de taille de pointeur signés et non signés, en particulier si cela ptrdiff_tsert déjà l'objectif d'un type entier de taille de pointeur signé.
Chris Lutz
8
La phrase clé est "sur presque toutes les plateformes", @Chris. Une implémentation est libre de restreindre les pointeurs à la plage 0xf000-0xffff - cela nécessite un intptr_t 16 bits mais seulement un ptrdiff_t 12/13 bits.
paxdiablo
29
@Chris, ce n'est que pour les pointeurs à l'intérieur du même tableau qu'il est bien défini de prendre leur différence. Donc, sur exactement les mêmes architectures segmentées 16 bits (le tableau doit vivre à l'intérieur d'un seul segment mais deux tableaux différents peuvent être dans des segments différents) les pointeurs doivent être de 4 octets mais les différences de pointeurs peuvent être de 2 octets!
Alex Martelli
6
@AlexMartelli: Sauf que les différences de pointeurs peuvent être positives ou négatives. La norme doit size_têtre d'au moins 16 bits, mais ptrdiff_td'au moins 17 bits (ce qui signifie en pratique qu'elle sera probablement d'au moins 32 bits).
Keith Thompson
3
Peu importe les architectures segmentées, qu'en est-il d'une architecture moderne comme x86-64? Les premières implémentations de cette architecture ne vous donnent qu'un espace adressable 48 bits, mais les pointeurs eux-mêmes sont un type de données 64 bits. Le plus grand bloc de mémoire contigu que vous pourriez raisonnablement adresser serait de 48 bits, donc je dois imaginer SIZE_MAXqu'il ne devrait pas être 2 ** 64. Cela utilise l'adressage plat, souvenez-vous; aucune segmentation n'est nécessaire pour avoir un décalage entre SIZE_MAXet la plage d'un pointeur de données.
Andon M. Coleman
89

Concernant votre déclaration:

"Le standard C garantit qu'il size_ts'agit d'un type pouvant contenir n'importe quel index de tableau. Cela signifie que, logiquement, il size_tdevrait pouvoir contenir n'importe quel type de pointeur."

Il s'agit en fait d'une erreur (une idée fausse résultant d'un raisonnement incorrect) (a) . Vous pensez peut-être que ce dernier découle du premier, mais ce n'est pas vraiment le cas.

Les pointeurs et les index de tableaux ne sont pas la même chose. Il est tout à fait plausible d'envisager une implémentation conforme qui limite les tableaux à 65536 éléments mais permet aux pointeurs d'adresser n'importe quelle valeur dans un espace d'adressage massif de 128 bits.

C99 indique que la limite supérieure d'une size_tvariable est définie par SIZE_MAXet cela peut être aussi bas que 65535 (voir C99 TR3, 7.18.3, inchangé en C11). Les pointeurs seraient assez limités s'ils étaient limités à cette gamme dans les systèmes modernes.

En pratique, vous constaterez probablement que votre hypothèse est vraie, mais ce n'est pas parce que la norme le garantit. Parce que cela ne le garantit pas.


(a) Ce n'est pas une forme d'attaque personnelle, soit dit en passant, expliquant simplement pourquoi vos déclarations sont erronées dans le contexte de la pensée critique. Par exemple, le raisonnement suivant n'est pas non plus valide:

Tous les chiots sont mignons. Cette chose est mignonne. Par conséquent, cette chose doit être un chiot.

La gentillesse ou non des chiots n'a aucune incidence ici, tout ce que je dis, c'est que les deux faits ne mènent pas à la conclusion, car les deux premières phrases permettent l'existence de choses mignonnes qui ne sont pas des chiots.

Ceci est similaire à votre première déclaration qui n'impose pas nécessairement la seconde.

paxdiablo
la source
Plutôt que de retaper ce que j'ai dit dans les commentaires d'Alex Martelli, je vais juste dire merci pour la clarification, mais réitérer la deuxième moitié de ma question (la partie ptrdiff_tvs. intptr_t).
Chris Lutz
5
@Ivan, comme pour la plupart des communications, il doit y avoir une compréhension commune de certains éléments de base. Si vous voyez cette réponse comme un "plaisir à pousser", je vous assure que c'est une mauvaise compréhension de mon intention. En supposant que vous faites référence à mon commentaire sur le «sophisme logique» (je ne vois aucune autre possibilité), cela était censé être une déclaration factuelle, pas une déclaration faite aux frais du PO. Si vous souhaitez suggérer des améliorations concrètes pour minimiser les risques de malentendus (plutôt qu'une simple plainte générale), je serais heureux d'envisager.
paxdiablo
1
@ivan_pozdeev - c'est une paire de modifications désagréables et drastiques, et je ne vois aucune preuve que paxdiablo "se moquait" de quiconque. Si j'étais l'OP, je retournerais ça tout de suite ....
ex nihilo
1
@Ivan, n'était pas vraiment satisfait des modifications que vous proposez, a annulé et a également tenté de supprimer toute infraction involontaire. Si vous avez d'autres modifications à proposer, je vous suggère de lancer une discussion afin que nous puissions en discuter.
paxdiablo
1
@paxdiablo ok, je suppose que "c'est en fait une erreur" est moins condescendant.
ivan_pozdeev
36

Je laisserai toutes les autres réponses s'affirmer concernant le raisonnement avec les limitations de segment, les architectures exotiques, etc.

La simple différence de noms n'est-elle pas une raison suffisante pour utiliser le bon type pour la bonne chose?

Si vous stockez une taille, utilisez size_t. Si vous stockez un pointeur, utilisez intptr_t. Une personne lisant votre code saura instantanément que "aha, c'est la taille de quelque chose, probablement en octets", et "oh, voici une valeur de pointeur stockée sous forme d'entier, pour une raison quelconque".

Sinon, vous pourriez simplement utiliser unsigned long(ou, en ces temps modernes ici unsigned long long), pour tout. La taille n'est pas tout, les noms de type ont une signification qui est utile car elle aide à décrire le programme.

se détendre
la source
Je suis d'accord, mais je considérais quelque chose d'un hack / trick (que je documenterais clairement, bien sûr) impliquant le stockage d'un type de pointeur dans un size_tchamp.
Chris Lutz
@MarkAdler Standard n'exige pas que les pointeurs soient entièrement représentables sous forme d'entiers: tout type de pointeur peut être converti en type entier. Sauf indication contraire, le résultat est défini par l'implémentation. Si le résultat ne peut pas être représenté dans le type entier, le comportement n'est pas défini. Le résultat n'a pas besoin d'être dans la plage de valeurs de n'importe quel type entier. Ainsi, seulement void*, intptr_tet uintptr_tsont garantis pour pouvoir représenter n'importe quel pointeur vers des données.
Andrew Svietlichnyy
12

Il est possible que la taille du plus grand tableau soit inférieure à celle d'un pointeur. Pensez aux architectures segmentées - les pointeurs peuvent être de 32 bits, mais un seul segment ne peut traiter que 64 Ko (par exemple l'ancienne architecture 8086 en mode réel).

Bien que ceux-ci ne soient plus couramment utilisés sur les ordinateurs de bureau, la norme C est destinée à prendre en charge même les petites architectures spécialisées. Il existe encore des systèmes embarqués en cours de développement avec des CPU 8 ou 16 bits par exemple.

Michael Burr
la source
Mais vous pouvez indexer des pointeurs comme des tableaux, alors devriez-vous size_tégalement pouvoir gérer cela? Ou les tableaux dynamiques dans certains segments éloignés seraient-ils encore limités à l'indexation dans leur segment?
Chris Lutz
L'indexation des pointeurs n'est techniquement prise en charge qu'à la taille du tableau vers lequel ils pointent - donc si un tableau est limité à une taille de 64 Ko, c'est tout ce que l'arithmétique du pointeur doit prendre en charge. Cependant, les compilateurs MS-DOS prenaient en charge un modèle de mémoire `` énorme '', où les pointeurs éloignés (pointeurs segmentés 32 bits) étaient manipulés afin qu'ils puissent adresser l'ensemble de la mémoire en tant que tableau unique - mais l'arithmétique effectuée pour les pointeurs en arrière-plan était assez moche - lorsque le décalage a augmenté au-delà d'une valeur de 16 (ou quelque chose), le décalage a été renvoyé à 0 et la partie de segment a été incrémentée.
Michael Burr
7
Lisez en.wikipedia.org/wiki/C_memory_model#Memory_segmentation et pleurez les programmeurs MS-DOS décédés afin que nous soyons libres.
Justicle
Le pire était que la fonction stdlib ne prenait pas en charge le mot-clé HUGE. 16bit MS-C pour toutes les strfonctions et Borland même pour les memfonctions ( memset, memcpy, memmove). Cela signifiait que vous pouviez écraser une partie de la mémoire lorsque le décalage débordait, ce qui était amusant à déboguer sur notre plate-forme intégrée.
Patrick Schlüter
@Justicle: L'architecture segmentée 8086 n'est pas bien prise en charge en C, mais je ne connais aucune autre architecture qui soit plus efficace dans les cas où un espace d'adressage de 1 Mo est suffisant mais un 64 Ko ne le serait pas. Certaines machines virtuelles Java modernes utilisent en fait un adressage très similaire au mode réel x86, utilisant des références d'objet 32 ​​bits décalées de 3 bits pour générer des adresses de base d'objet dans un espace d'adressage de 32 Go.
supercat
5

J'imagine (et cela vaut pour tous les noms de types) qu'il transmet mieux vos intentions dans le code.

Par exemple, même si unsigned shortet wchar_tsont de la même taille sous Windows (je pense), l'utilisation wchar_tau lieu de unsigned shortmontre l'intention que vous l'utiliserez pour stocker un caractère large, plutôt que juste un certain nombre arbitraire.

dreamlax
la source
Mais il y a une différence ici - sur mon système, wchar_test beaucoup plus grande qu'un unsigned shortdonc utiliser l'un pour l'autre serait erroné et créerait un problème de portabilité sérieux (et moderne), tandis que les problèmes de portabilité entre size_tet uintptr_tsemblent se situer dans les contrées lointaines de 1980-quelque chose (coup de couteau au hasard dans le noir à la date, là)
Chris Lutz
Touché! Mais là encore, size_tet uintptr_tont toujours des utilisations implicites dans leurs noms.
dreamlax
Ils le font, et je voulais savoir s'il y avait une motivation pour cela au-delà de la simple clarté. Et il s'avère que oui.
Chris Lutz
3

En regardant à la fois en arrière et en avant, et en rappelant que diverses architectures bizarres étaient dispersées dans le paysage, je suis presque sûr qu'elles essayaient d'envelopper tous les systèmes existants et de prévoir également tous les futurs systèmes possibles.

Tellement sûr, la façon dont les choses se sont arrangées, nous avons jusqu'à présent eu besoin de moins de types.

Mais même dans LP64, un paradigme assez courant, nous avions besoin de size_t et ssize_t pour l'interface d'appel système. On peut imaginer un système hérité ou futur plus contraint, où l'utilisation d'un type 64 bits complet coûte cher et peut-être voudra-t-il exécuter des opérations d'E / S de plus de 4 Go mais qui ont toujours des pointeurs 64 bits.

Je pense que vous devez vous demander: ce qui aurait pu être développé, ce qui pourrait arriver à l'avenir. (Peut-être des pointeurs à l'échelle du système distribué 128 bits sur Internet, mais pas plus de 64 bits dans un appel système, ou peut-être même une limite 32 bits "héritée". :-) Image que les systèmes hérités pourraient obtenir de nouveaux compilateurs C .. .

Regardez également ce qui existait à l'époque. Outre les modèles de mémoire en mode réel zillion 286, que diriez-vous des ordinateurs centraux de pointeur de mot / bit 18 bits CDC? Et la série Cray? Peu importe ILP64, LP64, LLP64 normal. (J'ai toujours pensé que Microsoft était prétentieux avec LLP64, il aurait dû être P64.) Je peux certainement imaginer un comité essayant de couvrir toutes les bases ...

DigitalRoss
la source
-9
int main(){
  int a[4]={0,1,5,3};
  int a0 = a[0];
  int a1 = *(a+1);
  int a2 = *(2+a);
  int a3 = 3[a];
  return a2;
}

Cela implique que intptr_t doit toujours remplacer size_t et vice versa.

Chris Becke
la source
10
Tout cela montre une bizarrerie syntaxique particulière de C. L'indexation de tableau est définie en termes de x [y] étant équivalent à * (x + y), et parce que a + 3 et 3 + a sont identiques en type et en valeur, vous pouvez utilisez 3 [a] ou a [3].
Fred Nurk