Recentemente, eu estava respondendo uma pergunta sobre o comportamento indefinido de fazer p < q
em C quando p
e q
são ponteiros para diferentes objetos / matrizes. Isso me fez pensar: C ++ tem o mesmo comportamento (indefinido) <
nesse caso, mas também oferece o modelo de biblioteca padrão std::less
que garante a mesma coisa que <
quando os ponteiros podem ser comparados e retorna uma ordem consistente quando não pode.
C oferece algo com funcionalidade semelhante que permita comparar com segurança ponteiros arbitrários (do mesmo tipo)? Tentei examinar o padrão C11 e não encontrei nada, mas minha experiência em C é uma ordem de magnitude menor que em C ++, para que eu pudesse ter perdido algo facilmente.
c
pointers
undefined-behavior
memory-model
memory-segmentation
Angew não está mais orgulhoso de SO
fonte
fonte
Respostas:
Nas implementações com um modelo de memória plana (basicamente tudo), a conversão para
uintptr_t
Just Work.(Mas consulte As comparações de ponteiros devem ser assinadas ou não assinadas no x86 de 64 bits? Para discutir se você deve tratar os ponteiros como assinados ou não, incluindo problemas de formação de ponteiros fora dos objetos que são UB em C.)
Mas sistemas com modelos de memória não-planos existem, e pensar sobre eles podem ajudar a explicar a situação atual, como C ++ ter especificações diferentes para
<
vs.std::less
.Parte do objetivo dos
<
ponteiros para separar objetos sendo UB em C (ou pelo menos não especificados em algumas revisões de C ++) é permitir máquinas estranhas, incluindo modelos de memória não plana.Um exemplo conhecido é o modo real x86-16, em que os ponteiros são segmentados: offset, formando um endereço linear de 20 bits via
(segment << 4) + offset
. O mesmo endereço linear pode ser representado por várias combinações diferentes seg: off.C ++
std::less
em ponteiros em ISAs estranhos pode precisar ser caro , por exemplo, "normalizar" um segmento: deslocamento em x86-16 para deslocamento <= 15. No entanto, não há uma maneira portátil de implementar isso. A manipulação necessária para normalizar umuintptr_t
(ou a representação de objeto de um objeto ponteiro) é específica da implementação.Mas mesmo em sistemas onde o C ++
std::less
precisa ser caro,<
não precisa ser. Por exemplo, supondo um modelo de memória "grande" em que um objeto se encaixe em um segmento,<
basta comparar a parte deslocada e nem mesmo se preocupar com a parte do segmento. (Ponteiros dentro do mesmo objeto terão o mesmo segmento e, caso contrário, o UB no C. C ++ 17 foi alterado para meramente "não especificado", o que ainda pode permitir ignorar a normalização e apenas comparar compensações.) Isso pressupõe que todos os ponteiros de qualquer parte de um objeto sempre use o mesmoseg
valor, nunca normalizando. Isso é o que você esperaria de uma ABI para um modelo de memória "grande" em oposição a "grande". (Veja a discussão nos comentários ).(Esse modelo de memória pode ter um tamanho máximo de objeto de 64 kiB, por exemplo, mas um espaço de endereço total máximo muito maior, com espaço para muitos desses objetos de tamanho máximo. O ISO C permite que as implementações tenham um limite no tamanho do objeto menor que o o valor máximo (não assinado)
size_t
pode representar,SIZE_MAX
por exemplo, mesmo em sistemas de modelo de memória plana, o GNU C limita o tamanho máximo do objeto paraPTRDIFF_MAX
que o cálculo do tamanho ignore o estouro assinado.) Consulte esta resposta e discussão nos comentários.Se você deseja permitir objetos maiores que um segmento, precisa de um modelo de memória "enorme" que precise se preocupar em exceder a parte deslocada de um ponteiro ao fazer um
p++
loop através de uma matriz ou ao fazer uma aritmética de indexação / ponteiro. Isso leva a códigos mais lentos em todos os lugares, mas provavelmente significaria quep < q
funcionaria para ponteiros para objetos diferentes, porque uma implementação direcionada a um modelo de memória "enorme" normalmente escolheria manter todos os ponteiros normalizados o tempo todo. Consulte O que há perto, longe e grandes indicadores? - alguns compiladores C reais para o modo real x86 tiveram uma opção de compilar para o modelo "enorme", onde todos os ponteiros assumiram o padrão de "enorme", a menos que declarado o contrário.A segmentação em modo real x86 não é o único modelo de memória não plana possível , é apenas um exemplo concreto útil para ilustrar como ele foi tratado pelas implementações de C / C ++. Na vida real, as implementações estenderam o ISO C com o conceito de ponteiros
far
vs.near
, permitindo que os programadores escolham quando podem simplesmente armazenar / passar pela parte offset de 16 bits, relativa a algum segmento de dados comum.Mas uma implementação ISO C pura teria que escolher entre um modelo de memória pequeno (tudo, exceto código no mesmo 64kiB com ponteiros de 16 bits) ou grande ou enorme, com todos os ponteiros de 32 bits. Alguns loops podem otimizar incrementando apenas a parte de deslocamento, mas os objetos ponteiros não podem ser otimizados para serem menores.
Se você soubesse qual era a manipulação mágica para qualquer implementação, poderia implementá-la em C puro . O problema é que sistemas diferentes usam endereços diferentes e os detalhes não são parametrizados por macros portáteis.
Ou talvez não: isso pode envolver a procura de algo em uma tabela de segmento especial ou algo assim, por exemplo, como o modo protegido x86, em vez do modo real, onde a parte do segmento do endereço é um índice, não um valor a ser mudado. Você pode configurar segmentos parcialmente sobrepostos no modo protegido, e as partes dos endereços do seletor de segmentos não seriam necessariamente ordenadas na mesma ordem que os endereços base do segmento correspondente. Obter um endereço linear de um ponteiro seg: off no modo protegido x86 pode envolver uma chamada do sistema, se o GDT e / ou LDT não estiverem mapeados em páginas legíveis em seu processo.
(É claro que os sistemas operacionais convencionais para x86 usam um modelo de memória simples, de modo que a base do segmento seja sempre 0 (exceto para armazenamento local de segmentos usando
fs
ougs
segmentos), e apenas a parte "deslocamento" de 32 ou 64 bits é usada como ponteiro .)Você pode adicionar manualmente o código para várias plataformas específicas, por exemplo, por padrão, supor flat ou
#ifdef
algo para detectar o modo real x86 e dividiruintptr_t
em metades de 16 bits paraseg -= off>>4; off &= 0xf;
depois combinar essas partes novamente em um número de 32 bits.fonte
p < q
é UB em C se eles apontam para objetos diferentes, não é? Eu sei quep - q
é.seg
valor desse objeto e um deslocamento que é> = o deslocamento no segmento em que esse objeto é iniciado. C torna UB fazer muito de qualquer coisa entre ponteiros para objetos diferentes, incluindo coisas comotmp = a-b
e depoisb[tmp]
acessara[0]
. Essa discussão sobre aliasing de ponteiro segmentado é um bom exemplo de por que essa escolha de design faz sentido.Uma vez tentei encontrar uma maneira de contornar isso e encontrei uma solução que funciona para sobrepor objetos e, na maioria dos outros casos, supondo que o compilador faça a coisa "usual".
Você pode primeiro implementar a sugestão em Como implementar o memmove no padrão C sem uma cópia intermediária? e, se isso não funcionar, é convertido em
uintptr
(um tipo de invólucro para umuintptr_t
ouunsigned long long
dependendo do queuintptr_t
está disponível) e obtém um resultado exato mais provável (embora provavelmente não importe de qualquer maneira):fonte
Não
Primeiro, vamos considerar apenas os ponteiros do objeto . Ponteiros de função trazem um conjunto totalmente diferente de preocupações.
2 ponteiros
p1, p2
podem ter codificações diferentes e apontar para o mesmo endereço,p1 == p2
mesmo quememcmp(&p1, &p2, sizeof p1)
não seja 0. Essas arquiteturas são raras.No entanto, a conversão desses ponteiros para
uintptr_t
não requer o mesmo resultado inteiro que leva a(uintptr_t)p1 != (uinptr_t)p2
.(uintptr_t)p1 < (uinptr_t)p2
em si é um código bem legal, pode não fornecer a funcionalidade esperada.Se o código realmente precisar comparar ponteiros não relacionados, forme uma função auxiliar
less(const void *p1, const void *p2)
e execute o código específico da plataforma.Possivelmente:
fonte