Independência ou morte? Morte!!

Quem foi que te disse que existe tal coisa de ‘linguagem de programação independente de arquitetura’? Aliás, quem foi que te disse que C é independe de arquitetura? É justamente o contrário! Linguagens como C foram criadas atreladas à arquitetura onde o desenvolvimento será feito.

Recentemente um de meus leitores me perguntou como saber o tamanho de tipos como int e long int se a quantidade de bits contidas nesses tipos depende do ambiente onde o compilador é usado. Por exemplo, no antigo MS-DOS o tipo int tinha 16 bits de tamanho, o mesmo tamanho de um tipo short. O tipo long tinha 32 bits. Mas, desde o Windows NT (Win32), o tipo int tem 32 bits e o long também! Em sistemas operacionais baseados no Unix o int normalmente tem 32 bits, exceto em casos bem específicos. O ponto aqui é que nada garante o tamanho padronizado dos tipos primitivos, exceto a arquitura alvo e o fabricante do compilador. A especificação ISO não força isso…

Embora a especificação da linguagem não nos diga nada sobre o tamanho dos tipos, ela nos diz para usarmos o header limits.h para descobrirmos. Em limits.h vamos encontrar um conjunto de símbolos definidos para o pré processador que informam ao desenvolvedor sobre os limites (o que mais?) dos tipos. Eis algumas constantes definidas por lá:

CHAR_BITS /* Número de bits no tipo 'char'. */
SCHAR_MIN, SCHAR_MAX /* Valores mínimo e máximo do 'signed char'. */
UCHAR_MAX /* Valor máximo que cabe num 'unsigned char'. */
SHORT_MIN, SHORT_MAX /* Valores mínimo e máximo do 'signed short'. */
INT_MIN, INT_MAX /* Adivinha só! */
UINT_MAX /* Não é difícil de adivinhar o que é isso, né? */
LONG_MIN, LONG_MAX, ULONG_MAX
LLONG_MIN, LLONG_MAX, ULLONG_MAX

Você reparou que as constantes relativas aos tipos float, double e long double foram deixadas de fora. Elas são definidas num outro header, chamado float.h

O GCC ainda define alguns símbolos default sem termos que usar limits.h e podemos vê-los através de uma simples linha de comando:

$ gcc -dM -E - < /dev/null | grep '\(MIN\|MAX\)[^A-Z]*'
#define __DBL_MIN_EXP__ (-1021)
#define __UINT_LEAST16_MAX__ 65535
#define __FLT_MIN__ 1.17549435082228750797e-38F
#define __INTMAX_C(c) c ## L
#define __UINT8_MAX__ 255
#define __WINT_MAX__ 4294967295U
#define __SIZE_MAX__ 18446744073709551615UL
#define __WCHAR_MAX__ 2147483647
#define __DBL_DENORM_MIN__ ((double)4.94065645841246544177e-324L)
#define __UINT_FAST64_MAX__ 18446744073709551615UL
#define __DBL_MIN_10_EXP__ (-307)
#define __UINT_FAST8_MAX__ 255
#define __DEC64_MAX_EXP__ 385
#define __UINT_LEAST64_MAX__ 18446744073709551615UL
#define __SHRT_MAX__ 32767
#define __LDBL_MAX__ 1.18973149535723176502e+4932L
#define __UINT_LEAST8_MAX__ 255
#define __UINTMAX_TYPE__ long unsigned int
#define __UINT32_MAX__ 4294967295U
#define __LDBL_MAX_EXP__ 16384
#define __WINT_MIN__ 0U
#define __SCHAR_MAX__ 127
#define __WCHAR_MIN__ (-__WCHAR_MAX__ - 1)
#define __LDBL_MIN__ 3.36210314311209350626e-4932L
#define __DEC32_MAX__ 9.999999E96DF
#define __INT32_MAX__ 2147483647
#define __DBL_MAX__ ((double)1.79769313486231570815e+308L)
#define __INT_FAST32_MAX__ 9223372036854775807L
#define __DEC32_MIN_EXP__ (-94)
#define __DEC128_MAX__ 9.999999999999999999999999999999999E6144DL
#define __INT_LEAST32_MAX__ 2147483647
#define __DEC32_MIN__ 1E-95DF
#define __DBL_MAX_EXP__ 1024
#define __PTRDIFF_MAX__ 9223372036854775807L
#define __LONG_LONG_MAX__ 9223372036854775807LL
#define __FLT_MIN_EXP__ (-125)
#define __DBL_MIN__ ((double)2.22507385850720138309e-308L)
#define __DEC128_MIN__ 1E-6143DL
#define __UINT16_MAX__ 65535
#define __DEC128_MIN_EXP__ (-6142)
#define __INT16_MAX__ 32767
#define __UINT64_MAX__ 18446744073709551615UL
#define __UINTMAX_C(c) c ## UL
#define __SIG_ATOMIC_MAX__ 2147483647
#define __DEC32_SUBNORMAL_MIN__ 0.000001E-95DF
#define __INT_FAST16_MAX__ 9223372036854775807L
#define __UINT_FAST32_MAX__ 18446744073709551615UL
#define __FLT_MAX_10_EXP__ 38
#define __LONG_MAX__ 9223372036854775807L
#define __DEC128_SUBNORMAL_MIN__ 0.000000000000000000000000000000001E-6143DL
#define __DEC64_MAX__ 9.999999999999999E384DD
#define __INT_LEAST16_MAX__ 32767
#define __INT64_MAX__ 9223372036854775807L
#define __UINT_LEAST32_MAX__ 4294967295U
#define __DEC32_MAX_EXP__ 97
#define __INT_FAST8_MAX__ 127
#define __INTPTR_MAX__ 9223372036854775807L
#define __SIG_ATOMIC_MIN__ (-__SIG_ATOMIC_MAX__ - 1)
#define __UINTPTR_MAX__ 18446744073709551615UL
#define __DEC64_MIN_EXP__ (-382)
#define __INT_FAST64_MAX__ 9223372036854775807L
#define __INT_MAX__ 2147483647
#define __FLT_MAX_EXP__ 128
#define __INT_LEAST64_MAX__ 9223372036854775807L
#define __DEC64_MIN__ 1E-383DD
#define __LDBL_MIN_EXP__ (-16381)
#define __INT_LEAST8_MAX__ 127
#define __LDBL_MAX_10_EXP__ 4932
#define __INTMAX_MAX__ 9223372036854775807L
#define __FLT_DENORM_MIN__ 1.40129846432481707092e-45F
#define __INT8_MAX__ 127
#define __FLT_MAX__ 3.40282346638528859812e+38F
#define __FLT_MIN_10_EXP__ (-37)
#define __INTMAX_TYPE__ long int
#define __DEC128_MAX_EXP__ 6145
#define __UINTMAX_MAX__ 18446744073709551615UL
#define __DBL_MAX_10_EXP__ 308
#define __LDBL_DENORM_MIN__ 3.64519953188247460253e-4951L
#define __DEC64_SUBNORMAL_MIN__ 0.000000000000001E-383DD
#define __LDBL_MIN_10_EXP__ (-4931)
#define __UINT_FAST16_MAX__ 18446744073709551615UL

A maioria desses símbolos tem equivalência direta com o que está no header limits.h, mas são específicos do GCC. Reparou que existem definições extras como __INT_FAST32_MAX? O GCC define isso em stdint.h como garantia para o uso de tipos garantidamente em conformidade com os registradores do seu processador: O tipo int32_t tem garantidamente 32 bits de tamanho, mas o tipo int32_fast_t, além de ter 32 bits, cabe, garantidamente, num registrador. Não vale à pena usar esses tipos ‘fast’, já que eles só existem no GCC.

Ahhhh… Fiz uma filtragem pelos nomes ‘MIN’ e ‘MAX’ no comando acima porque a lista de símbolos pré definidas é enorme. Existem coisas úteis como definições da aquitetura atual (__x86_64, __SSE__, __SSE_MATH__, __ELF__, __LP64__, __BYTE_ORDER__ etc), alguns macros, algumas definições de características do compilador, da variação do C usada; o tamanho dos tipos primitivos (em bytes):

$ gcc -dM -E - < /dev/null | grep 'SIZEOF'
#define __SIZEOF_INT__ 4
#define __SIZEOF_POINTER__ 8
#define __SIZEOF_LONG__ 8
#define __SIZEOF_LONG_DOUBLE__ 16
#define __SIZEOF_SIZE_T__ 8
#define __SIZEOF_WINT_T__ 4
#define __SIZEOF_PTRDIFF_T__ 8
#define __SIZEOF_FLOAT__ 4
#define __SIZEOF_SHORT__ 2
#define __SIZEOF_INT128__ 16
#define __SIZEOF_WCHAR_T__ 4
#define __SIZEOF_DOUBLE__ 8
#define __SIZEOF_LONG_LONG__ 8

Bem como o tipo usado para size_t: __SIZE_TYPE__. Tem outras coisinhas por lá, mas é prudente usar essas definições testando se o compilador em questão é, de fato, o GCC:

#ifdef __GNUC__
  /* definições para o GCC aqui. */
#else
  /* Definições para outros compiladores aqui. */
#endif

Outros tipos padronizados da libc:

É claro que essa “falta” de padronização nos tamanhos dos tipos levou a ISO a definir dois headers para definição de tipos padrão de inteiros. Os headers são stdint.h e stdbool.h. Na especificação C99 o tipo _Bool é definido sem o auxílio de nenhum typedef, mas stdbool.h define bool.

ATENÇÃO! O tipo _Bool, bem como seu typedef bool pode ter 1 bit ou mais de tamanho. SIM! 1 bit! Dependendo do compilador e da arquitetura variáveis desse tipo podem ser mantidos em flags, por exemplo. Mas, em termos de armazenamento na memória, ele pode ter um byte ou mais.

O outro header, stdint.h define os tipos integrais de tamanho fixo: int8_t, int16_t, int32_t, int64_t e suas variações unsigned (colocando um ‘u’ na frente do nome do tipo). Outra coisa que vale a pena ser mencionada sobre o padrão ISO é que o tipo __int128 também é padronizado, mas não consta de stdint.h por motivos óbvios.

Neste ponto você pode ter ouvido falar de um outro header homologado pela ISO chamado inttypes.h. Ele inclui stdint.h e define algumas constantes e funções para manipulação dos typedefs citados acima. Por exemplo, ao usar printf() as formatações %ld podem não se aplicar ao tipo int64_t (aplicam-se no Linux, mas não no Windows, por exemplo). Daí inttypes.h define constantes nomeadas PRI?#, onde ‘?’ deve ser substituído por ‘d’, ‘i’, ‘u’, ‘x’ ou ‘o’ ao estilo da função printf() (por isso o ‘PRI’!) e ‘#’ deve ser substituido pelo tamanho. Assim, para imprimir o conteúdo de um uint64_t deveríamos fazer:

extern int64_t x;
...
printf("Valor: " PRId64 "\n", x);

Existem constantes semelhantes para o scanf() nomeadas SCN?#, onde ‘?’ e ‘#’ tem a mesma semântica que citada acima.

Windows é bem intencionado, mas

Pois é… eis porque eu citei o Windows lá em cima: Uma das coisas interessantes que a Microsoft fez com o Software Development Kit do Windows é definir seus próprios tipos. Já se perguntou porque a M$ usa LONG ao invés de long? Ou LPSTR ao invés de char *? A ideia é tornar o SDK independente de arquitetura (Windows roda em MIPS, PowerPC, além de Intel). Num dos headers do SDK ela define esses tipos maiúsculos e o desenvolvedor, em teroria, não precisa se preocupar com inconsistências de tipos. Assim, um LONG, no Windows, não importa qual for a arquitetura, terá 32 bits de tamanho, assim como um INT.

Com os headers stdint.h e inttypes.h esse tipo de estratégia ficou meio que obsoleta, mas é uma ideia…

Anúncios