c printf portability

¿Cómo imprimir tipos de tamaño desconocido como ino_t?



printf portability (4)

Con frecuencia, experimento situaciones en las que quiero imprimir con printf el valor de un tipo entero de tamaño definido por la implementación (como ino_t o time_t ). En este momento, utilizo un patrón como este para esto:

#include <inttypes.h> ino_t ino; /* variable of unknown size */ printf("%" PRIuMAX, (uintmax_t)ino);

Este enfoque funciona hasta ahora, pero tiene un par de desventajas:

  • Tengo que saber si el tipo que estoy intentando imprimir está firmado o sin firmar.
  • Tengo que usar un modelo moldeado que posiblemente amplíe mi código.

¿Hay una mejor estrategia?


Dado que ya está utilizando el encabezado C99, existe la posibilidad de usar el especificador de formato de ancho exacto según el sizeof(T) y la verificación firmada / sin firmar. Sin embargo, esto debe hacerse después de la fase de preprocesamiento (por lo que, lamentablemente, el operador ## no se puede usar aquí para construir el token PRI). Aquí hay una idea:

#include <inttypes.h> #define IS_SIGNED(T) (((T)-1) < 0) /* determines if integer type is signed */ ... const char *fs = NULL; size_t bytes = sizeof(T); if (IS_SIGNED(T)) switch (bytes) { case 1: fs = PRId8; break; case 2: fs = PRId16; break; case 4: fs = PRId32; break; case 8: fs = PRId64; break; } else switch (bytes) { case 1: fs = PRIu8; break; case 2: fs = PRIu16; break; case 4: fs = PRIu32; break; case 8: fs = PRIu64; break; }

Con este método, la conversión ya no es necesaria, sin embargo, la cadena de formato se debe construir manualmente antes de pasarla a printf (es decir, sin concatenación de cadena automática) Aquí hay un ejemplo de trabajo:

#include <stdio.h> #include <inttypes.h> #define IS_SIGNED(T) (((T)-1) < 0) /* using GCC extension: Statement Expr */ #define FMT_CREATE(T) ({ / const char *fs = NULL; / size_t bytes = sizeof(ino_t); / / if (IS_SIGNED(T)) / switch (bytes) { / case 1: fs = "%" PRId8; break; / case 2: fs = "%" PRId16; break; / case 4: fs = "%" PRId32; break; / case 8: fs = "%" PRId64; break; / } / else / switch (bytes) { / case 1: fs = "%" PRIu8; break; / case 2: fs = "%" PRIu16; break; / case 4: fs = "%" PRIu32; break; / case 8: fs = "%" PRIu64; break; / } / fs; / }) int main(void) { ino_t ino = 32; printf(FMT_CREATE(ino_t), ino); putchar(''/n''); return 0; }

Tenga en cuenta que esto requiere algunos pequeños trucos de Statement Expr , pero también puede haber alguna otra forma (este es el "precio" para que sea genérico).

EDITAR:

Aquí está la segunda versión, que no requiere una extensión de compilador específica (no te preocupes, no puedo leerla también) usando una macro similar a una función:

#include <stdio.h> #include <inttypes.h> #define IS_SIGNED(T) (((T)-1) < 0) #define S(T) (sizeof(T)) #define FMT_CREATE(T) / (IS_SIGNED(T) / ? (S(T)==1?"%"PRId8:S(T)==2?"%"PRId16:S(T)==4?"%"PRId32:"%"PRId64) / : (S(T)==1?"%"PRIu8:S(T)==2?"%"PRIu16:S(T)==4?"%"PRIu32:"%"PRIu64)) int main(void) { ino_t ino = 32; printf(FMT_CREATE(ino_t), ino); putchar(''/n''); return 0; }

Tenga en cuenta que el operador condicional ha dejado la asociatividad (por lo tanto, evalúa de izquierda a derecha según lo previsto).


El "tamaño" de un tipo entero no es relevante aquí, pero su rango de valores.

Como aparentemente intentaste, aún, es posible convertir a uintmax_t e intmax_t para resolver fácilmente cualquier ambigüedad en la llamada a printf() .

El problema de los tipos firmados o no firmados se puede resolver de una manera fácil:

  • Todas las operaciones de enteros sin signo funcionan en el módulo "N" para algún valor positivo N, dependiendo del tipo. Implica que todo resultado que involucre solo tipos enteros sin signo da un valor no negativo.
  • Para detectar si una variable x tiene un tipo con signo o sin signo, es suficiente verificar si x y -x son valores no negativos.

Por ejemplo:

if ( (x>=0) && (-x>=0) ) printf("x has unsigned type"); else printf("x has signed type");

Ahora, podemos escribir algunas macros:

(Editado: el nombre y la expresión de la macro han cambiado)

#include <inttypes.h> #include <limits.h> #define fits_unsigned_type(N) ( (N >= 0) && ( (-(N) >= 0) || ((N) <= INT_MAX) ) ) #define smartinteger_printf(N) / (fits_unsigned_type(N)? printf("%ju",(uintmax_t)(N)): printf("%jd",(intmax_t) (N)) ) // .... ino_t x = -3; printf("The value is: "); smartinteger_printf(x); //.....

Nota: el carácter con o sin signo de una variable no es bien detectado por la macro anterior cuando el valor es 0. Pero en este caso todo funciona bien, porque 0 tiene la misma representación de bits en los tipos con signo o sin signo.

La primera macro se puede usar para detectar si el tipo subyacente de un objeto aritmético tiene un tipo no ordenado o no.
Este resultado se usa en la segunda macro para elegir la forma en que el objeto se imprime en la pantalla.

1ª REEDICION:

  • Como señaló Pascal Cuoq en su comentario, las promociones de números enteros deben tomarse de acuerdo con los caracteres sin signo y short valores cortos en el rango de int . Esto es equivalente a preguntar si el valor está en el rango 0 a INT_MAX .

Así que he cambiado el nombre de la macro a fits_signed_type .
Además, he modificado la macro para tener en cuenta los valores positivos de int .

La macro fits_unsigned_type puede indicar si un objeto tiene un tipo entero sin signo o no en la mayoría de los casos.

  • Si el valor es negativo, obviamente el tipo no está sin unsigned .
  • Si el valor N es positivo entonces
    • si -N es positivo, entonces N tiene un tipo unsigned ,
    • Si -N es negativo, pero N está en el rango de 0 a INT_MAX , entonces el tipo de N podría estar signed o unsigned , pero encajaría en el rango de valores positivos de int , que encaja en el rango de uintmax_t .

2ª REEDICION:

Ir parece que hay aquí enfoques para resolver el mismo problema. Mi enfoque toma en cuenta el rango de valores y las reglas de promoción de enteros para producir el valor impreso correcto con printf() . Por otro lado, el enfoque de Grzegorz Szpetkowski determina el carácter firmado de un tipo en forma recta. Me gustan ambos.


Usando macros genéricas de tipo C11, es posible construir la cadena de formato en tiempo de compilación, por ejemplo:

#include <inttypes.h> #include <limits.h> #include <stdint.h> #include <stdio.h> #define PRI3(B,X,A) _Generic((X), / unsigned char: B"%hhu"A, / unsigned short: B"%hu"A, / unsigned int: B"%u"A, / unsigned long: B"%lu"A, / unsigned long long: B"%llu"A, / signed char: B"%hhd"A, / short: B"%hd"A, / int: B"%d"A, / long: B"%ld"A, / long long: B"%lld"A) #define PRI(X) PRI3("",(X),"") #define PRIFMT(B,X,A) PRI3(B,(X),A),(X) int main () { signed char sc = SCHAR_MIN; unsigned char uc = UCHAR_MAX; short ss = SHRT_MIN; unsigned short us = USHRT_MAX; int si = INT_MIN; unsigned ui = UINT_MAX; long sl = LONG_MIN; unsigned long ul = ULONG_MAX; long long sll = LLONG_MIN; unsigned long long ull = ULLONG_MAX; size_t z = SIZE_MAX; intmax_t sj = INTMAX_MIN; uintmax_t uj = UINTMAX_MAX; (void) printf(PRIFMT("signed char : ", sc, "/n")); (void) printf(PRIFMT("unsigned char : ", uc, "/n")); (void) printf(PRIFMT("short : ", ss, "/n")); (void) printf(PRIFMT("unsigned short : ", us, "/n")); (void) printf(PRIFMT("int : ", si, "/n")); (void) printf(PRIFMT("unsigned int : ", ui, "/n")); (void) printf(PRIFMT("long : ", sl, "/n")); (void) printf(PRIFMT("unsigned long : ", ul, "/n")); (void) printf(PRIFMT("long long : ", sll, "/n")); (void) printf(PRIFMT("unsigned long long: ", ull, "/n")); (void) printf(PRIFMT("size_t : ", z, "/n")); (void) printf(PRIFMT("intmax_t : ", sj, "/n")); (void) printf(PRIFMT("uintmax_t : ", uj, "/n")); }

Sin embargo, existe un problema potencial: si hay tipos distintos de los listados (es decir, que unsigned versiones unsigned y unsigned de char , short , int , long y long long ), esto no funciona para esos tipos. Tampoco es posible agregar tipos como size_t e intmax_t a la macro genérica de tipo "por si acaso", ya que causará un error si son typedef d de uno de los tipos ya enumerados. Dejé el caso default especificar para que la macro genere un error en tiempo de compilación cuando no se encuentre un tipo coincidente.

Sin embargo, como se ve en el programa de ejemplo, size_t e intmax_t funcionan bien en plataformas en las que son iguales a uno de los tipos listados (por ejemplo, igual long ). De manera similar, no hay problema si, por ejemplo, long y long long , o long e int , son del mismo tipo. Pero una versión más segura podría ser simplemente convertir a intmax_t o uintmax_t acuerdo con la firmeza (como se ve en otras respuestas), y hacer una macro genérica de tipo con solo esas opciones ...

Un problema cosmético es que la macro genérica de tipo se expande con paréntesis alrededor del literal de cadena, impidiendo la concatenación con literales de cadena adyacentes de la manera habitual. Esto evita cosas como:

(void) printf("var = " PRI(var) "/n", var); // does not work!

De ahí la macro PRIFMT con prefijo y sufijo incluidos para el caso común de imprimir una sola variable:

(void) printf(PRIFMT("var = ", var, "/n"));

(Tenga en cuenta que sería sencillo expandir esta macro con los tipos no enteros admitidos por printf , por ejemplo, double , char * …)


#include <inttypes.h> ino_t ino; /* variable of unknown size */ /* ... */ printf("%" PRIuMAX, (uintmax_t)ino);

Eso funcionará (con algunas condiciones; ver más abajo), pero usaría:

printf("%ju", (uintmax_t)ino);

El modificador de longitud j

Especifica que el siguiente especificador de conversión d , i , o , u , x , o X aplica a un argumento intmax_t o uintmax_t ; o que un siguiente especificador de conversión se aplique a un puntero a un argumento intmax_t .

También hay z y t modificadores para size_t y ptrdiff_t (y sus correspondientes tipos con signo / sin signo), respectivamente.

Y personalmente, encuentro que las macros de cadena de formato definidas en <inttypes.h> feas y difíciles de recordar, por lo que prefiero "%ju" o "%jd" .

Como mencionó, es útil saber si el tipo ( ino_t en este caso) está firmado o no. Si no lo sabes, es posible averiguarlo:

#include <stdio.h> #include <stdint.h> #include <sys/types.h> #define IS_SIGNED(type) ((type)-1 < (type)0) #define DECIMAL_FORMAT(type) (IS_SIGNED(type) ? "%jd" : "%ju") #define CONVERT_TO_MAX(type, value) / (IS_SIGNED(type) ? (intmax_t)(value) : (uintmax_t)(value)) #define PRINT_VALUE(type, value) / (printf(DECIMAL_FORMAT(type), CONVERT_TO_MAX(type, (value)))) int main(void) { ino_t ino = 42; PRINT_VALUE(ino_t, ino); putchar(''/n''); }

aunque eso puede ser una exageración. Si está seguro de que el tipo es más estrecho que 64 bits, puede convertir el valor a intmax_t , y el valor se conservará. O puede usar uintmax_t y obtener resultados bien definidos para todos los valores, aunque imprimir -1 como 18446744073709551615 (2 64 -1) puede ser un poco confuso.

Todo esto funciona solo si su implementación de C es compatible con <stdint.h> y el modificador de longitud j para printf , es decir, si es compatible con C99. No todos los compiladores lo hacen ( toser Microsoft tos ). Para C90, los tipos de enteros más anchos son long y unsigned long , y puede convertirlos a esos y usar "%ld" y / o "%lu" . __STDC_VERSION__ puede probar el cumplimiento de C99 utilizando la macro predefinida __STDC_VERSION__ , aunque algunos compiladores pre-C99 todavía pueden admitir tipos más anchos que long y unsigned long como una extensión.