No sentido estrito, esse não é um comportamento indefinido, mas definido pela implementação. Portanto, apesar de desaconselhável, se você planeja oferecer suporte a arquiteturas não convencionais, provavelmente poderá fazê-lo.
A cotação padrão dada por interjay é boa, indicando UB, mas é apenas o segundo melhor acerto na minha opinião, pois trata da aritmética ponteiro-ponteiro (engraçado, um é explicitamente um UB, enquanto o outro não). Há um parágrafo que trata diretamente da operação na pergunta:
[expr.post.incr] / [expr.pre.incr]
O operando deve ser [...] ou um ponteiro para um tipo de objecto completamente definido.
Oh, espere um momento, um tipo de objeto completamente definido? Isso é tudo? Quero dizer, realmente, tipo ? Então você não precisa de um objeto?
É preciso um pouco de leitura para realmente encontrar uma dica de que algo lá dentro pode não ser tão bem definido. Porque até agora, parece que você está perfeitamente autorizado a fazê-lo, sem restrições.
[basic.compound] 3
faz uma declaração sobre o tipo de ponteiro que um pode ter e, sendo nenhum dos outros três, o resultado da sua operação claramente se enquadra em 3.4: ponteiro inválido .
No entanto, não diz que você não tem um ponteiro inválido. Pelo contrário, lista algumas condições normais muito comuns (por exemplo, duração do fim do armazenamento) em que os ponteiros se tornam regularmente inválidos. Então isso é aparentemente uma coisa permissível de acontecer. E realmente:
[basic.stc] 4 Indirecionamento
através de um valor inválido de ponteiro e passando um valor inválido de ponteiro para uma função de desalocação têm comportamento indefinido. Qualquer outro uso de um valor de ponteiro inválido possui um comportamento definido pela implementação.
Estamos fazendo um "qualquer outro" lá, então não é um comportamento indefinido, mas definido pela implementação, portanto geralmente permitido (a menos que a implementação diga explicitamente algo diferente).
Infelizmente, esse não é o fim da história. Embora o resultado líquido não mude mais a partir de agora, ele fica mais confuso, quanto mais você procurar "ponteiro":
[basic.compound]
Um valor válido de um tipo de ponteiro de objeto representa o endereço de um byte na memória ou um ponteiro nulo. Se um objeto do tipo T estiver localizado em um endereço, [...] é dito que A aponta para esse objeto, independentemente de como o valor foi obtido .
[Nota: Por exemplo, o endereço um após o final de uma matriz seria considerado apontar para um objeto não relacionado do tipo de elemento da matriz que pode estar localizado nesse endereço. [...]]
Leia como: OK, quem se importa! Enquanto um ponteiro apontar para algum lugar na memória , eu estou bem?
[basic.stc.dynamic.safety] Um valor de ponteiro é um ponteiro derivado com segurança [blá blá]
Leia como: OK, derivado com segurança, qualquer que seja. Não explica o que é isso, nem diz que eu realmente preciso. Derivado com segurança. Aparentemente, ainda posso ter indicadores não-derivados com segurança. Suponho que desferenciá-los provavelmente não seria uma boa idéia, mas é perfeitamente permitido tê-los. Não diz o contrário.
Uma implementação pode ter uma segurança relaxada do ponteiro; nesse caso, a validade de um valor de ponteiro não depende se é um valor de ponteiro derivado com segurança.
Ah, então não importa, exatamente o que eu pensava. Mas espere ... "não pode"? Isso significa que também pode . Como eu sei?
Como alternativa, uma implementação pode ter uma segurança estrita do ponteiro; nesse caso, um valor de ponteiro que não seja um valor de ponteiro derivado com segurança é um valor de ponteiro inválido, a menos que o objeto completo referenciado tenha duração de armazenamento dinâmico e tenha sido declarado anteriormente acessível
Espere, então é possível que eu precise chamar declare_reachable()
cada ponteiro? Como eu sei?
Agora, você pode converter para intptr_t
, que está bem definido, fornecendo uma representação inteira de um ponteiro derivado com segurança. Para o qual, é claro, sendo um número inteiro, é perfeitamente legítimo e bem definido para incrementá-lo como desejar.
E sim, você pode converter as intptr_t
costas em um ponteiro, que também é bem definido. Apenas, não sendo o valor original, não é mais garantido que você tenha um ponteiro derivado com segurança (obviamente). Ainda assim, de acordo com a letra do padrão, embora definido pela implementação, isso é uma coisa 100% legítima a ser feita:
[expr.reinterpret.cast] 5
Um valor do tipo integral ou do tipo enumeração pode ser explicitamente convertido em um ponteiro. Um ponteiro convertido em um número inteiro de tamanho [...] suficiente e retornado ao mesmo valor original do tipo [...] ponteiro; mapeamentos entre ponteiros e números inteiros são definidos pela implementação.
A pegada
Ponteiros são apenas números inteiros comuns, apenas você os usa como ponteiros. Oh, se isso fosse verdade!
Infelizmente, existem arquiteturas onde isso não é verdade, e apenas gerar um ponteiro inválido (sem desreferenciá-lo, apenas tê-lo em um registro de ponteiro) causará uma armadilha.
Então essa é a base da "implementação definida". Isso e o fato de incrementar um ponteiro sempre que você quiser, como você pode, naturalmente, causar um estouro, com o qual o padrão não quer lidar. O final do espaço de endereço do aplicativo pode não coincidir com o local do estouro, e você nem sabe se existe um excesso de ponteiros para uma arquitetura específica. Em suma, é uma bagunça de pesadelo, sem nenhuma relação com os possíveis benefícios.
Lidar com a condição de um objeto passado, por outro lado, é fácil: a implementação deve simplesmente garantir que nenhum objeto seja alocado para que o último byte no espaço de endereço seja ocupado. Portanto, isso é bem definido, pois é útil e trivial de garantir.