Os tipos de largura variável foram substituídos por tipos fixos no C moderno?

21

Hoje me deparei com um ponto interessante em uma revisão sobre o Code Review . O @Veedrac recomendou nesta resposta que tipos de tamanho variável (por exemplo, inte long) sejam substituídos por tipos de tamanho fixo como uint64_te uint32_t. Citação dos comentários dessa resposta:

Os tamanhos de int e long (e, portanto, os valores que eles podem conter) dependem da plataforma. Por outro lado, int32_t sempre tem 32 bits. Usar int apenas significa que seu código funciona de maneira diferente em plataformas diferentes, o que geralmente não é o que você deseja.

O raciocínio por trás do padrão que não corrige os tipos comuns é parcialmente explicado aqui por @supercat. C foi escrito para ser portátil em todas as arquiteturas, ao contrário da montagem que geralmente era usada para programação de sistemas na época.

Eu acho que a intenção do projeto era originalmente que cada tipo, exceto int, fosse a menor coisa que pudesse lidar com números de vários tamanhos, e que esse seria o tamanho "de uso geral" mais prático que poderia lidar com +/- 32767.

Quanto a mim, sempre usei inte não me preocupo com as alternativas. Eu sempre pensei que é o tipo mais com melhor desempenho, final da história. O único lugar que achei que a largura fixa seria útil é ao codificar dados para armazenamento ou transferência em uma rede. Eu raramente vi tipos de largura fixa no código escrito por outras pessoas também.

Estou preso nos anos 70 ou existe realmente uma justificativa para usar intna era do C99 e além?

jacwah
fonte
1
Uma parcela das pessoas apenas imita outras. Eu acredito que a maioria do código do tipo bit fixo foi feita sem consciência. Não há razão para definir o tamanho nem para não. Eu tenho código feito principalmente em plataformas de 16 bits (MS-DOS e Xenix dos anos 80), que apenas compilam e executam hoje em qualquer 64 e benefícios do novo tamanho de palavra e endereçamento, apenas compilando-o. Isso significa que a serialização para exportar / importar dados é um projeto de arquitetura muito importante para mantê-lo portátil.
Luciano

Respostas:

7

Existe um mito comum e perigoso de que tipos como uint32_tsalvar programadores de terem que se preocupar com o tamanho de int. Embora seja útil que o Comitê de Padrões defina um meio de declarar números inteiros com semântica independente de máquina, tipos não assinados como uint32_tsemântica são muito frouxos para permitir que o código seja escrito de maneira limpa e portátil; além disso, tipos assinados como int32semântica são, para muitas aplicações, formas definidas de forma desnecessária e, portanto, impedem o que de outra forma seriam otimizações úteis.

Considere, por exemplo:

uint32_t upow(uint32_t n, uint32_t exponent)
{
  while(exponent--)
    n*=n;
  return n;
}

int32_t spow(int32_t n, uint32_t exponent)
{
  while(exponent--)
    n*=n;
  return n;
}

Em máquinas nas quais intnão é possível conter 4294967295 ou 18446744065119617025, a primeira função será definida para todos os valores de ne exponent, e seu comportamento não será afetado pelo tamanho de int; além disso, o padrão não exigirá que ele produza comportamento diferente em máquinas com qualquer tamanho de int Alguns valores de ne exponent, no entanto, fará com que invoque o Comportamento indefinido em máquinas nas quais 4294967295 é representável como int18446744065119617025, mas não é.

A segunda função produzirá Comportamento indefinido para alguns valores de ne exponentem máquinas onde intnão pode conter 4611686014132420609, mas produzirá um comportamento definido para todos os valores de ne exponentem todas as máquinas em que puder (as especificações int32_timplicam que o comportamento de quebra de complemento de dois em máquinas onde é menor que int).

Historicamente, mesmo que o Padrão não dissesse nada sobre o que os compiladores deveriam fazer com o inttransbordamento upow, os compiladores teriam consistentemente produzido o mesmo comportamento como se inttivesse sido grande o suficiente para não transbordar. Infelizmente, alguns compiladores mais recentes podem tentar "otimizar" programas, eliminando comportamentos não exigidos pelo Padrão.

supercat
fonte
3
Qualquer pessoa que queira implementar manualmente pow, lembre-se de que este código é apenas um exemplo e não serve exponent=0!
Mark Hurd
1
Eu acho que você deveria estar usando o operador de decremento de prefixo e não o postfix, atualmente ele está fazendo 1 multiplicação extra, por exemplo exponent=1, resultará em n sendo multiplicado por si só uma vez, uma vez que o decremento é realizado após a verificação, se o incremento for realizado antes da verificação ( ex. - expoente), nenhuma multiplicação será realizada e n será retornado.
ALXGTV
2
@ MarkHurd: A função é mal nomeada, pois o que realmente calcula é N^(2^exponent), mas os cálculos do formulário N^(2^exponent)são frequentemente usados ​​no cálculo das funções de exponenciação, e a exponenciação mod-4294967296 é útil para coisas como calcular o hash da concatenação de duas seqüências de caracteres cuja hashes são conhecidos.
precisa
1
@ALXGTV: A função era para ilustrar algo que calculava algo relacionado à energia. O que ele realmente calcula é N ^ (2 ^ expoente), que faz parte do cálculo eficiente de N ^ expoente, e pode falhar mesmo que N seja pequeno (a multiplicação repetida de a uint32_tpor 31 nunca produzirá UB, mas a eficiência A maneira de calcular 31 ^ N envolve cálculos de 31 ^ (2 ^ N), o que será necessário.
supercat 15/15
Eu não acho que este seja um bom argumento. O objetivo não é definir funções definidas para todas as entradas, sensíveis ou não; é ser capaz de raciocinar sobre tamanhos e transbordamentos. int32_tàs vezes, tendo definido o estouro e às vezes não, que é o que você parece estar mencionando, parece ter uma importância mínima em relação ao fato de que isso me permite pensar em como evitar o estouro. E se você deseja um estouro definido, é provável que esteja querendo que o módulo de resultado tenha algum valor fixo - portanto, você está usando tipos de largura fixa de qualquer maneira.
Veedrac
4

Para valores intimamente relacionados a ponteiros (e, portanto, à quantidade de memória endereçável), como tamanhos de buffer, índices de matriz e Windows ' lParam, faz sentido ter um tipo inteiro com um tamanho dependente da arquitetura. Portanto, tipos de tamanho variável ainda são úteis. É por isso que temos os typedefs size_t, ptrdiff_t, intptr_t, etc. Eles têm que ser typedefs porque nenhum dos built-in C inteiro tipos precisa ser ponteiro porte.

Então a questão é realmente se char, short, int, long, e long longainda são úteis.

IME, ainda é comum os programas C e C ++ usarem intpara a maioria das coisas. E na maioria das vezes (ou seja, quando seus números estão na faixa de ± 32.767 e você não possui requisitos rigorosos de desempenho), isso funciona perfeitamente.

Mas e se você precisar trabalhar com números no intervalo de 17 a 32 bits (como as populações das grandes cidades)? Você poderia usar int, mas isso codificaria uma dependência de plataforma. Se você quiser longseguir rigorosamente o padrão, poderá usar o que é garantido em pelo menos 32 bits.

O problema é que o padrão C não especifica nenhum tamanho máximo para um tipo inteiro. Existem implementações nas quais longsão 64 bits, o que duplica o uso de memória. E se esses longelementos forem elementos de uma matriz com milhões de itens, você estragará a memória como um louco.

Portanto, intnem longé um tipo adequado para usar aqui se você deseja que seu programa seja multiplataforma e com eficiência de memória. Enter int_least32_t.

  • Seu compilador I16L32 oferece 32 bits long, evitando os problemas de truncamento doint
  • Seu compilador I32L64 oferece 32 bits int, evitando a perda de memória do 64 bits long.
  • O seu compilador I36L72 oferece uma instalação de 36 bits. int

OTOH, suponha que você não precise de grandes números ou matrizes enormes, mas precisa de velocidade. E intpode ser grande o suficiente em todas as plataformas, mas não é necessariamente o tipo mais rápido: os sistemas de 64 bits geralmente ainda têm 32 bits int. Mas você pode usar int_fast16_te obter o tipo “mais rápido”, se é int, longou long long.

Portanto, existem casos de uso práticos para os tipos de <stdint.h>. Os tipos inteiros padrão não significam nada. Especialmente long, que pode ter 32 ou 64 bits e pode ou não ser grande o suficiente para conter um ponteiro, dependendo do capricho dos escritores do compilador.

dan04
fonte
Um problema com tipos como uint_least32_té que suas interações com outros tipos são especificadas de maneira ainda mais fraca do que as de uint32_t. IMHO, o Padrão deve definir tipos como uwrap32_te unum32_t, com a semântica que qualquer compilador que define o tipo uwrap32_t, deve promover como um tipo não assinado, essencialmente nos mesmos casos em que seria promovido se intfossem 32 bits, e qualquer compilador que define o tipo unum32_tdeve garantir que promoções aritméticas básicas sempre o convertem em um tipo assinado capaz de manter seu valor.
Supercat
Além disso, o Padrão também poderia definir tipos cujo armazenamento e aliasing fossem compatíveis com intN_te uintN_t, e cujos comportamentos definidos seriam consistentes com intN_te uintN_t, mas que dariam aos compiladores alguma liberdade no caso de códigos atribuídos a valores fora de seu intervalo [permitindo semânticas semelhantes àquelas talvez pretendido uint_least32_t, mas sem incertezas, como adicionar um uint_least16_te um int32_tproduziria um resultado assinado ou não assinado.
Supercat