size_t vs. uintptr_t

246

El estándar C garantiza que size_tes un tipo que puede contener cualquier índice de matriz. Esto significa que, lógicamente, size_tdebería poder contener cualquier tipo de puntero. Leí en algunos sitios que encontré en Google que esto es legal y / o siempre debería funcionar:

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

Entonces, en C99, el estándar introdujo los tipos intptr_ty uintptr_t, que son tipos con y sin signo garantizados para poder mantener punteros:

uintptr_t p = (size_t) v;

Entonces, ¿cuál es la diferencia entre usar size_ty uintptr_t? Ambos no están firmados, y ambos deberían poder contener cualquier tipo de puntero, por lo que parecen funcionalmente idénticos. ¿Existe alguna razón real convincente para usar uintptr_t(o mejor aún, a void *) en lugar de una size_t, aparte de la claridad? En una estructura opaca, donde el campo será manejado solo por funciones internas, ¿hay alguna razón para no hacerlo?

Del mismo modo, ptrdiff_tha sido un tipo con signo capaz de contener las diferencias de puntero y, por lo tanto, capaz de contener la mayoría de los punteros, entonces, ¿en qué se diferencia intptr_t?

¿No están todos estos tipos básicamente sirviendo versiones trivialmente diferentes de la misma función? Si no, ¿por qué? ¿Qué no puedo hacer con uno de ellos que no puedo hacer con otro? Si es así, ¿por qué C99 agregó dos tipos esencialmente superfluos al lenguaje?

Estoy dispuesto a ignorar los indicadores de función, ya que no se aplican al problema actual, pero siéntase libre de mencionarlos, ya que tengo la sospecha de que serán fundamentales para la respuesta "correcta".

Chris Lutz
fuente

Respuestas:

236

size_tes un tipo que puede contener cualquier índice de matriz. Esto significa que, lógicamente, size_t debería poder contener cualquier tipo de puntero

¡No necesariamente! Volvamos a los días de las arquitecturas segmentadas de 16 bits, por ejemplo: una matriz podría estar limitada a un solo segmento (lo size_tque haría un 16 bits ) PERO podría tener múltiples segmentos (por intptr_tlo que sería necesario un tipo de 32 bits para elegir el segmento, así como el desplazamiento dentro de él). Sé que estas cosas suenan raras en estos días de arquitecturas no segmentadas uniformemente direccionables, pero el estándar DEBE abarcar una variedad más amplia que "lo que es normal en 2009", ¡ya sabes! -)

Alex Martelli
fuente
66
Esto, junto con los muchos otros que saltaron a la misma conclusión, explica la diferencia entre size_ty, uintptr_tpero ¿qué pasa con ptrdiff_ty intptr_t, no podrían ambos almacenar el mismo rango de valores en casi cualquier plataforma? ¿Por qué tener tipos enteros de tamaño de puntero con signo y sin signo, especialmente si ptrdiff_tya tiene el propósito de un tipo de entero de tamaño de puntero con signo?
Chris Lutz
8
La frase clave es "en casi cualquier plataforma", @Chris. Una implementación es libre de restringir los punteros al rango 0xf000-0xffff; esto requiere un intptr_t de 16 bits pero solo un ptrdiff_t de 12/13 bits.
paxdiablo
29
@Chris, solo para los punteros dentro de la misma matriz está bien definido tomar la diferencia. Entonces, exactamente en las mismas arquitecturas segmentadas de 16 bits (la matriz debe vivir dentro de un solo segmento pero dos matrices diferentes pueden estar en segmentos diferentes) los punteros deben tener 4 bytes, ¡pero las diferencias de puntero podrían ser de 2 bytes!
Alex Martelli
66
@AlexMartelli: Excepto que las diferencias de puntero pueden ser positivas o negativas. El estándar requiere size_ttener al menos 16 bits, pero ptrdiff_tdebe tener al menos 17 bits (lo que en la práctica significa que probablemente tendrá al menos 32 bits).
Keith Thompson
3
No importa las arquitecturas segmentadas, ¿qué pasa con una arquitectura moderna como x86-64? Las primeras implementaciones de esta arquitectura solo le dan un espacio direccionable de 48 bits, pero los punteros mismos son un tipo de datos de 64 bits. El bloque de memoria contiguo más grande que podría abordar razonablemente sería de 48 bits, por lo que SIZE_MAXdebo imaginar que no debería ser 2 ** 64. Esto está usando el direccionamiento plano, eso sí; no es necesaria la segmentación para tener una discrepancia entre SIZE_MAXy el rango de un puntero de datos.
Andon M. Coleman
89

Con respecto a su declaración:

"El estándar C garantiza que size_tes un tipo que puede contener cualquier índice de matriz. Esto significa que, lógicamente, size_tdebería poder contener cualquier tipo de puntero".

Esto es en realidad una falacia (una idea errónea resultante de un razonamiento incorrecto) (a) . Puede pensar que este último se desprende del primero, pero ese no es realmente el caso.

Los punteros y los índices de matriz no son lo mismo. Es bastante posible imaginar una implementación conforme que limite las matrices a 65536 elementos pero permita que los punteros dirijan cualquier valor a un espacio de direcciones masivo de 128 bits.

C99 establece que el límite superior de una size_tvariable está definido por SIZE_MAXy esto puede ser tan bajo como 65535 (ver C99 TR3, 7.18.3, sin cambios en C11). Los punteros serían bastante limitados si estuvieran restringidos a este rango en los sistemas modernos.

En la práctica, probablemente encontrará que su suposición se cumple, pero eso no se debe a que el estándar lo garantice. Porque en realidad no lo garantiza.


(a) Por cierto, esta no es una forma de ataque personal, solo indica por qué sus declaraciones son erróneas en el contexto del pensamiento crítico. Por ejemplo, el siguiente razonamiento tampoco es válido:

Todos los cachorros son lindos. Esta cosa es linda. Por lo tanto, esta cosa debe ser un cachorro.

La ternura o no de los cachorros no tiene nada que ver aquí, todo lo que digo es que los dos hechos no conducen a la conclusión, porque las dos primeras oraciones permiten la existencia de cosas lindas que no son cachorros.

Esto es similar a su primera declaración, no necesariamente obligando a la segunda.

paxdiablo
fuente
En lugar de volver a escribir lo que dije en los comentarios para Alex Martelli, solo diré gracias por la aclaración, pero reiteraré la segunda mitad de mi pregunta (la parte ptrdiff_tvs. intptr_t).
Chris Lutz
55
@Ivan, como ocurre con la mayoría de las comunicaciones, debe haber una comprensión compartida de ciertos elementos básicos. Si ve esta respuesta como "burlarse", le aseguro que es un malentendido de mi intención. Suponiendo que se está refiriendo a mi comentario de 'falacia lógica' (no puedo ver ninguna otra posibilidad), eso se entiende como una declaración de hechos, no como una declaración hecha a expensas del OP. Si desea sugerir alguna mejora concreta para minimizar la posibilidad de malentendidos (en lugar de solo una queja general), me complacería considerarlo.
paxdiablo
1
@ivan_pozdeev: ese es un par de ediciones desagradables y drásticas, y no veo evidencia de que paxdiablo se "burlara" de nadie. Si yo fuera el OP, revertiría esto de inmediato ...
ex nihilo
1
@Ivan, no estaba muy contento con las ediciones que propusiste, retrocedí y también traté de eliminar cualquier delito no intencionado. Si tiene otros cambios que ofrecer, le sugiero que inicie un chat para que podamos discutirlo.
paxdiablo
1
@paxdiablo está bien, supongo que "esto es realmente una falacia" es menos condescendiente.
ivan_pozdeev
36

Dejaré que todas las otras respuestas se defiendan por sí mismas con respecto al razonamiento con limitaciones de segmento, arquitecturas exóticas, etc.

¿No es la simple diferencia en los nombres una razón suficiente para usar el tipo apropiado para lo correcto?

Si está almacenando una talla, úsela size_t. Si está almacenando un puntero, úselo intptr_t. Una persona que lea su código sabrá instantáneamente que "aha, este es un tamaño de algo, probablemente en bytes", y "oh, aquí hay un valor de puntero que se almacena como un entero, por alguna razón".

De lo contrario, podría usar unsigned long(o, en estos tiempos modernos aquí unsigned long long) para todo. El tamaño no lo es todo, los nombres de los tipos tienen un significado que es útil ya que ayuda a describir el programa.

relajarse
fuente
Estoy de acuerdo, pero estaba considerando algo así como un truco / truco (que claramente documentaría, por supuesto) que implica almacenar un tipo de puntero en un size_tcampo.
Chris Lutz
@MarkAdler Standard no requiere que los punteros sean representables como enteros por completo: cualquier tipo de puntero se puede convertir a un tipo entero. Excepto como se especificó previamente, el resultado está definido por la implementación. Si el resultado no puede representarse en el tipo entero, el comportamiento es indefinido. El resultado no necesita estar en el rango de valores de ningún tipo entero. Por lo tanto, solo void*, intptr_ty uintptr_testán garantizados para poder representar cualquier puntero a los datos.
Andrew Svietlichnyy
12

Es posible que el tamaño de la matriz más grande sea más pequeño que un puntero. Piense en arquitecturas segmentadas: los punteros pueden ser de 32 bits, pero un solo segmento puede ser capaz de direccionar solo 64 KB (por ejemplo, la antigua arquitectura 8086 en modo real).

Si bien estos ya no se usan comúnmente en máquinas de escritorio, el estándar C está diseñado para admitir incluso arquitecturas pequeñas y especializadas. Todavía se están desarrollando sistemas integrados con CPU de 8 o 16 bits, por ejemplo.

Michael Burr
fuente
Pero puede indexar punteros al igual que las matrices, por lo que size_ttambién debería poder manejar eso. ¿O las matrices dinámicas en algún segmento lejano todavía se limitarían a la indexación dentro de su segmento?
Chris Lutz
La indexación de punteros solo es técnicamente compatible con el tamaño de la matriz a la que apuntan, por lo que si una matriz está limitada a un tamaño de 64 KB, eso es todo lo que la aritmética de puntero necesita soportar. Sin embargo, los compiladores de MS-DOS admitían un modelo de memoria 'enorme', donde se manipulaban los punteros lejanos (punteros segmentados de 32 bits) para que pudieran abordar la totalidad de la memoria como una sola matriz, pero la aritmética hecha a los punteros detrás de escena era bastante feo: cuando el desplazamiento aumentó más allá de un valor de 16 (o algo así), el desplazamiento volvió a 0 y la parte del segmento se incrementó.
Michael Burr
77
Lea en.wikipedia.org/wiki/C_memory_model#Memory_segmentation y llore por los programadores de MS-DOS que murieron para que podamos ser libres.
Justicle
Lo peor fue que la función stdlib no se ocupó de la palabra clave ENORME. 16 bits MS-C para todas las strfunciones y Borland incluso para las memfunciones ( memset, memcpy, memmove). Esto significaba que podía sobrescribir parte de la memoria cuando se desbordaba el desplazamiento, lo cual fue divertido de depurar en nuestra plataforma integrada.
Patrick Schlüter
@Justicle: La arquitectura segmentada 8086 no está bien soportada en C, pero no conozco ninguna otra arquitectura que sea más eficiente en los casos en que un espacio de direcciones de 1 MB sea suficiente pero no sea de 64 KB. Algunas JVM modernas en realidad utilizan el direccionamiento de manera muy similar al modo real x86, utilizando referencias de objetos de 32 bits desplazadas a la izquierda de 3 bits para generar direcciones base de objetos en un espacio de direcciones de 32 GB.
supercat
5

Me imagino (y esto se aplica a todos los nombres de tipo) que transmite mejor sus intenciones en código.

Por ejemplo, aunque unsigned shorty wchar_tson del mismo tamaño en Windows (creo), usar en wchar_tlugar de unsigned shortmuestra la intención de que lo use para almacenar un carácter ancho, en lugar de solo un número arbitrario.

dreamlax
fuente
Pero hay una diferencia aquí: en mi sistema, wchar_tes mucho más grande que unsigned shortusarlo, por lo que usar uno para el otro sería erróneo y crearía un problema de portabilidad grave (y moderno), mientras que el problema de portabilidad entre size_ty uintptr_tparece estar en tierras lejanas de 1980-algo (puñalada al azar en la oscuridad en la fecha, allí)
Chris Lutz
Touché! Pero, de nuevo, size_ty uintptr_ttodavía tienen usos implícitos en sus nombres.
dreamlax
Lo hacen, y quería saber si había una motivación para esto más allá de la simple claridad. Y resulta que hay.
Chris Lutz
3

Mirando hacia atrás y hacia adelante, y recordando que varias arquitecturas extrañas estaban dispersas por el paisaje, estoy bastante seguro de que estaban tratando de abarcar todos los sistemas existentes y también proporcionar todos los posibles sistemas futuros.

Así que, por la forma en que se resolvieron las cosas, hasta ahora no hemos necesitado tantos tipos.

Pero incluso en LP64, un paradigma bastante común, necesitábamos size_t y ssize_t para la interfaz de llamada del sistema. Uno puede imaginar un sistema heredado o futuro más restringido, en el que el uso de un tipo completo de 64 bits es costoso y es posible que deseen realizar despejes en operaciones de E / S de más de 4 GB, pero aún tienen punteros de 64 bits.

Creo que debe preguntarse: qué podría haberse desarrollado, qué podría venir en el futuro. (Tal vez punteros de todo el sistema distribuido de 128 bits en Internet, pero no más de 64 bits en una llamada al sistema, o tal vez incluso un límite "heredado" de 32 bits. :-) Imagen de que los sistemas heredados pueden obtener nuevos compiladores de C .. .

Además, mira lo que existía en ese momento. Además de los modelos de memoria en modo real zillion 286, ¿qué hay de los mainframes CDC de 60 bits de palabra / puntero de 18 bits? ¿Qué tal la serie Cray? No importa lo normal ILP64, LP64, LLP64. (Siempre pensé que Microsoft era pretencioso con LLP64, debería haber sido P64.) Ciertamente, puedo imaginar un comité tratando de cubrir todas las bases ...

DigitalRoss
fuente
-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;
}

Lo que implica que intptr_t siempre debe sustituir size_t y viceversa.

Chris Becke
fuente
10
Todo esto muestra una peculiaridad de sintaxis particular de C. La indexación de matriz se define en términos de que x [y] es equivalente a * (x + y), y debido a que a + 3 y 3 + a son idénticos en tipo y valor, puede use 3 [a] o a [3].
Fred Nurk