c++ - O incremento de um ponteiro para uma matriz dinâmica do tamanho 0 é indefinido?




pointers undefined-behavior (2)

Eu acho que você já tem a resposta; Se você olhar um pouco mais fundo: você disse que incrementar um iterador off-the-end é UB assim: Esta resposta está no que é um iterador?

O iterador é apenas um objeto que possui um ponteiro e, incrementando esse iterador, está realmente incrementando o ponteiro que possui. Assim, em muitos aspectos, um iterador é tratado em termos de um ponteiro.

int arr [] = {0,1,2,3,4,5,6,7,8,9};

int * p = arr; // p aponta para o primeiro elemento em arr

++ p; // p aponta para arr [1]

Assim como podemos usar iteradores para atravessar os elementos em um vetor, também podemos usar ponteiros para atravessar os elementos em uma matriz. Obviamente, para fazer isso, precisamos obter ponteiros para o primeiro e um após o último elemento. Como acabamos de ver, podemos obter um ponteiro para o primeiro elemento usando a própria matriz ou pegando o endereço do primeiro elemento. Podemos obter um ponteiro completo usando outra propriedade especial de matrizes. Podemos levar o endereço do elemento inexistente um após o último elemento de uma matriz:

int * e = & arr [10]; // ponteiro após o último elemento em arr

Aqui usamos o operador subscrito para indexar um elemento inexistente; arr tem dez elementos, então o último elemento em arr está na posição 9. do índice. A única coisa que podemos fazer com esse elemento é pegar seu endereço, o que fazemos para inicializar e. Como um iterador off-the-end (§ 3.4.1, p. 106), um ponteiro off-the-end não aponta para um elemento. Como resultado, não podemos desreferenciar ou incrementar um ponteiro de ponta.

Este é do C ++ primer 5 edição de Lipmann.

Então é UB, não faça isso.

AFAIK, embora não possamos criar uma matriz de memória estática de tamanho 0, mas podemos fazê-lo com outras dinâmicas:

int a[0]{}; // Compile-time error
int* p = new int[0]; // Is well-defined

Como eu li, p atua como um elemento de fim passado. Eu posso imprimir o endereço que p aponta.

if(p)
    cout << p << endl;
  • Embora eu tenha certeza de que não podemos desreferenciar esse ponteiro (último-último-elemento) como não podemos com iteradores (último-último-elemento), mas o que não tenho certeza é se está incrementando esse ponteiro p ? Um comportamento indefinido (UB) é semelhante aos iteradores?

    p++; // UB?

No sentido estrito, esse não é um comportamento indefinido, mas definido pela implementação. Portanto, apesar de desaconselhável, se você planeja suportar arquiteturas não convencionais, provavelmente pode 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.post.incr]
O operando deve ser [...] ou um ponteiro para um tipo de objeto 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 que tipo de ponteiro um pode ter e, sendo nenhum dos outros três, o resultado da sua operação seria claramente classificado 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
O direcionamento através de um valor de ponteiro inválido e a passagem de um valor de ponteiro inválido para uma função de desalocação têm um 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 ponteiros 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() em 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 o intptr_t novamente em um ponteiro, o 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 de 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 fazer referência a ele, 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.







dynamic-arrays