size_t vs. uintptr_t

246

O padrão C garante que esse size_té um tipo que pode conter qualquer índice de matriz. Isso significa que, logicamente, size_tdeve ser capaz de manter qualquer tipo de ponteiro. Li em alguns sites que achei no Google que isso é legal e / ou sempre deve funcionar:

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

Então, em C99, o padrão introduziu os tipos intptr_te uintptr_t, que são assinados e não assinados, com a garantia de poder conter ponteiros:

uintptr_t p = (size_t) v;

Então, qual é a diferença entre usar size_te uintptr_t? Ambos não são assinados e devem poder manter qualquer tipo de ponteiro, para que pareçam funcionalmente idênticos. Existe alguma razão real e convincente para usar uintptr_t(ou melhor ainda, a void *) ao invés de a size_t, além da clareza? Em uma estrutura opaca, onde o campo será tratado apenas por funções internas, existe alguma razão para não fazer isso?

Da mesma forma, ptrdiff_tfoi um tipo assinado capaz de conter diferenças de ponteiro e, portanto, capaz de conter quase todo o ponteiro, então como é diferente intptr_t?

Todos esses tipos não servem basicamente versões trivialmente diferentes da mesma função? Se não, por que? O que não posso fazer com um deles que não posso fazer com outro? Se sim, por que o C99 adicionou dois tipos essencialmente supérfluos ao idioma?

Estou disposto a ignorar os ponteiros de função, pois eles não se aplicam ao problema atual, mas fique à vontade para mencioná-los, pois tenho uma suspeita de que eles serão essenciais para a resposta "correta".

Chris Lutz
fonte

Respostas:

236

size_té um tipo que pode conter qualquer índice de matriz. Isso significa que, logicamente, size_t deve poder manter qualquer tipo de ponteiro

Não necessariamente! Lembre-se dos dias de arquiteturas segmentadas de 16 bits, por exemplo: uma matriz pode ser limitada a um único segmento (o que size_tseria de 16 bits ), mas você poderia ter vários segmentos (portanto, intptr_tseria necessário escolher um tipo de 32 bits) o segmento, bem como o deslocamento dentro dele). Sei que essas coisas parecem estranhas nos dias de arquiteturas não segmentadas e endereçáveis ​​de maneira uniforme, mas o padrão DEVE atender a uma variedade maior do que "o que é normal em 2009", você sabe! -)

Alex Martelli
fonte
6
Isso, junto com os numerosos outros que chegaram à mesma conclusão, explica a diferença entre size_te, uintptr_tmas ptrdiff_te quanto a intptr_t- eles não seriam capazes de armazenar o mesmo intervalo de valores em quase qualquer plataforma? Por que os tipos de números inteiros do tamanho de um ponteiro assinados e não assinados, principalmente se ptrdiff_tjá servem ao propósito de um tipo inteiro do tamanho de um ponteiro assinado
23711 Chris Lutz
8
Frase-chave existe "em quase qualquer plataforma", @ Chris. Uma implementação é livre para restringir os ponteiros ao intervalo 0xf000-0xffff - isso requer um intptr_t de 16 bits, mas apenas um ptrdiff_t de 12/13 bits.
paxdiablo
29
@ Chris, apenas para ponteiros dentro da mesma matriz é bem definido para fazer a diferença. Portanto, exatamente nas mesmas arquiteturas segmentadas de 16 bits (a matriz deve residir em um único segmento, mas duas matrizes diferentes podem estar em segmentos diferentes) os ponteiros devem ter 4 bytes, mas as diferenças dos ponteiros podem ter 2 bytes!
Alex Martelli
6
@AlexMartelli: Exceto que as diferenças do ponteiro podem ser positivas ou negativas. O padrão requer size_tpelo menos 16 bits, mas ptrdiff_tpelo menos 17 bits (o que na prática significa que provavelmente haverá pelo menos 32 bits).
Keith Thompson
3
Arquiteturas segmentadas que não importam, e uma arquitetura moderna como o x86-64? As implementações iniciais dessa arquitetura oferecem apenas um espaço endereçável de 48 bits, mas os ponteiros são do tipo de dados de 64 bits. O maior bloco contíguo de memória que você poderia razoavelmente endereçar seria de 48 bits, portanto, devo imaginar SIZE_MAXque não deve ser 2 ** 64. Isso é usar endereçamento simples, lembre-se; nenhuma segmentação é necessária para haver uma incompatibilidade entre SIZE_MAXe o intervalo de um ponteiro de dados.
Andon M. Coleman
89

Em relação à sua declaração:

"O padrão C garante que size_té um tipo que pode conter qualquer índice de matriz. Isso significa que, logicamente, size_tdeve ser capaz de armazenar qualquer tipo de ponteiro."

Na verdade, isso é uma falácia (um equívoco resultante de raciocínio incorreto) (a) . Você pode pensar que o último segue do primeiro, mas esse não é realmente o caso.

Ponteiros e índices de matriz não são a mesma coisa. É bastante plausível prever uma implementação em conformidade que limita as matrizes a 65536 elementos, mas permite que os ponteiros endereçam qualquer valor em um espaço de endereçamento massivo de 128 bits.

C99 afirma que o limite superior de uma size_tvariável é definido por SIZE_MAXe pode ser tão baixo quanto 65535 (consulte C99 TR3, 7.18.3, inalterado em C11). Os ponteiros seriam bastante limitados se estivessem restritos a essa faixa nos sistemas modernos.

Na prática, você provavelmente descobrirá que sua suposição é válida, mas não é porque o padrão a garanta. Porque na verdade não garante isso.


(a) A propósito, essa não é uma forma de ataque pessoal, apenas afirmando por que suas declarações são errôneas no contexto do pensamento crítico. Por exemplo, o seguinte raciocínio também é inválido:

Todos os filhotes são fofos. Essa coisa é fofa. Portanto, essa coisa deve ser um filhote de cachorro.

A fofura ou não dos filhotes não tem influência aqui, tudo o que afirmo é que os dois fatos não levam à conclusão, porque as duas primeiras frases permitem a existência de coisas fofas que não são filhotes.

Isso é semelhante à sua primeira declaração, não necessariamente obrigando a segunda.

paxdiablo
fonte
Em vez de redigitar o que eu disse nos comentários de Alex Martelli, vou apenas agradecer pelo esclarecimento, mas reiterar a segunda metade da minha pergunta ( parte ptrdiff_tvs. intptr_t).
22720 Chris Lutz
5
@Ivan, como na maioria das comunicações, é preciso haver um entendimento compartilhado de certos itens básicos. Se você vê esta resposta como "zombando", garanto que é um mal-entendido da minha intenção. Supondo que você esteja se referindo ao meu comentário de "falácia lógica" (não vejo outra possibilidade), isso foi uma afirmação factual, não uma afirmação feita às custas do OP. Se você gostaria de sugerir alguma melhoria concreta para minimizar a possibilidade de mal-entendidos (em vez de apenas uma reclamação geral), ficaria feliz em considerar.
paxdiablo
1
@ivan_pozdeev - esse é um par de edições desagradáveis ​​e drásticas, e não vejo evidências de que paxdiablo estivesse "zombando" de ninguém. Se eu fosse o OP, eu reverteria isso de volta ....
ex nihilo
1
@Ivan, não estava muito satisfeito com as edições que você propôs, reverteu e também tentou remover qualquer ofensa não intencional. Se você tiver outras alterações a oferecer, sugiro iniciar um bate-papo para que possamos discutir.
22418
1
@ paxdiablo ok, acho que "isso é realmente uma falácia" é menos paternalista.
ivan_pozdeev
36

Vou deixar que todas as outras respostas sejam válidas em relação ao raciocínio com limitações de segmento, arquiteturas exóticas e assim por diante.

A simples diferença de nomes não é motivo suficiente para usar o tipo apropriado para a coisa correta?

Se você estiver armazenando um tamanho, use size_t. Se você estiver armazenando um ponteiro, use intptr_t. Uma pessoa que estiver lendo seu código saberá instantaneamente que "aha, esse é o tamanho de algo, provavelmente em bytes" e "oh, aqui está um valor de ponteiro sendo armazenado como um número inteiro, por algum motivo".

Caso contrário, você poderia apenas usar unsigned long(ou, nestes tempos modernos aqui unsigned long long) para tudo. Tamanho não é tudo, nomes de tipo têm significado útil, pois ajuda a descrever o programa.

descontrair
fonte
Concordo, mas estava considerando um truque / truque (que claramente documentaria, é claro) envolvendo o armazenamento de um tipo de ponteiro em um size_tcampo.
23711 Chris Lutz
O @MarkAdler Standard não exige que os ponteiros sejam representáveis ​​como números inteiros: qualquer tipo de ponteiro pode ser convertido em um tipo inteiro. Exceto conforme especificado anteriormente, o resultado é definido pela implementação. Se o resultado não puder ser representado no tipo inteiro, o comportamento será indefinido. O resultado não precisa estar no intervalo de valores de nenhum tipo inteiro. Assim, apenas void*, intptr_te uintptr_tsão garantidos para ser capaz de representar qualquer ponteiro para dados.
Andrew Svietlichnyy
12

É possível que o tamanho da maior matriz seja menor que um ponteiro. Pense em arquiteturas segmentadas - os ponteiros podem ter 32 bits, mas um único segmento pode ser capaz de endereçar apenas 64 KB (por exemplo, a antiga arquitetura 8086 em modo real).

Embora eles não sejam mais usados ​​em computadores desktop, o padrão C destina-se a suportar até arquiteturas pequenas e especializadas. Ainda existem sistemas embarcados sendo desenvolvidos com CPUs de 8 ou 16 bits, por exemplo.

Michael Burr
fonte
Mas você pode indexar ponteiros como matrizes, então size_ttambém deve ser capaz de lidar com isso? Ou matrizes dinâmicas em algum segmento distante ainda seriam limitadas à indexação em seu segmento?
23710 Chris Lutz
Os ponteiros de indexação são tecnicamente suportados apenas pelo tamanho da matriz para a qual apontam - portanto, se uma matriz é limitada a um tamanho de 64 KB, é tudo o que a aritmética do ponteiro precisa suportar. No entanto, os compiladores do MS-DOS eram compatíveis com um modelo de memória 'enorme', em que os ponteiros distantes (ponteiros segmentados de 32 bits) eram manipulados para que pudessem endereçar toda a memória como uma única matriz - mas a aritmética feita aos ponteiros nos bastidores era bastante feio - quando o deslocamento aumentou além de um valor de 16 (ou algo assim), o deslocamento foi retornado a 0 e a parte do segmento foi aumentada.
Michael Burr
7
Leia en.wikipedia.org/wiki/C_memory_model#Memory_segmentation e chore pelos programadores do MS-DOS que morreram para que possamos ficar livres.
Justicle 23/09/09
O pior foi que a função stdlib não tratou da palavra-chave HUGE. 16bit MS-C para todas as strfunções e Borland mesmo para as memfunções ( memset, memcpy, memmove). Isso significava que você poderia sobrescrever parte da memória quando o deslocamento excedeu, o que foi divertido de depurar em nossa plataforma incorporada.
Patrick Schlüter
@ Artigo: A arquitetura segmentada 8086 não é bem suportada em C, mas não conheço nenhuma outra arquitetura que seja mais eficiente nos casos em que um espaço de endereço de 1 MB é suficiente, mas um de 64K não seria. Algumas JVMs modernas realmente usam o endereçamento muito parecido com o modo real x86, usando referências de objetos de 32 bits deslocadas para a esquerda em 3 bits para gerar endereços base de objetos em um espaço de endereço de 32GB.
precisa
5

Eu imaginaria (e isso vale para todos os nomes de tipos) que transmite melhor suas intenções no código.

Por exemplo, embora unsigned shorte wchar_tsejam do mesmo tamanho no Windows (acho), usar em wchar_tvez de unsigned shortmostra a intenção de usá-lo para armazenar um caractere amplo, em vez de apenas um número arbitrário.

dreamlax
fonte
Mas há uma diferença aqui - no meu sistema, wchar_té muito maior do que uma, unsigned shortportanto, usar um para o outro seria errôneo e criaria uma séria (e moderna) preocupação de portabilidade, enquanto a portabilidade se preocupa entre size_te uintptr_tparece estar nas terras distantes de 1980, algo (facada aleatória no escuro sobre a data, lá)
Chris Lutz
Touché! Mas, novamente, size_te uintptr_tainda tem usos implícitos em seus nomes.
dreamlax 23/09/09
Eles fazem, e eu queria saber se havia uma motivação para isso além da simples clareza. E acontece que existe.
23711 Chris Lutz
3

Olhando para trás e para frente, e lembrando que várias arquiteturas excêntricas estavam espalhadas pela paisagem, tenho certeza de que elas estavam tentando envolver todos os sistemas existentes e também fornecer todos os possíveis sistemas futuros.

Tão certo, da maneira como as coisas se resolveram, até agora não precisávamos de tantos tipos.

Mas mesmo no LP64, um paradigma bastante comum, precisávamos de size_t e ssize_t para a interface de chamada do sistema. Pode-se imaginar um sistema legado ou futuro mais restrito, em que o uso de um tipo completo de 64 bits é caro e eles podem querer realizar operações de E / S maiores que 4 GB, mas ainda possuem indicadores de 64 bits.

Acho que você deve se perguntar: o que pode ter sido desenvolvido, o que pode vir no futuro. (Talvez indicadores de 128 bits do sistema distribuído na Internet, mas não mais que 64 bits em uma chamada do sistema, ou talvez até mesmo um limite "legado" de 32 bits. :-) Imagem de que sistemas legados podem obter novos compiladores C .. .

Além disso, observe o que existia na época. Além dos zilhões de modelos de memória em modo real 286, e os mainframes CDC de 60 bits / ponteiro de 18 bits? E a série Cray? Não importa ILP64, LP64, LLP64 normal. (Eu sempre pensei que a Microsoft era pretensiosa com o LLP64, deveria ter sido o P64.) Certamente posso imaginar um comitê tentando cobrir todas as bases ...

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

Isso implica que intptr_t sempre deve substituir size_t e vice-versa.

Chris Becke
fonte
10
Tudo isso mostra uma peculiaridade particular da sintaxe de C. A indexação da matriz é definida em termos de x [y] equivalente a * (x + y) e, como a + 3 e 3 + a são idênticos em tipo e valor, você pode use 3 [a] ou a [3].
Fred Nurk