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

34

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, page como um elemento de passado passado. Eu posso imprimir o endereço que paponta para.

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?
Itachi Uchiwa
fonte
4
UB "... Quaisquer outras situações (ou seja, tentativas de gerar um ponteiro que não esteja apontando para um elemento da mesma matriz ou que esteja além do final) invocam um comportamento indefinido ..." de: en.cppreference.com / w / cpp / language / operator_arithmetic
Richard Critten
3
Bem, isso é semelhante a um std::vectoritem com 0. begin()já é igual a, end()portanto, você não pode incrementar um iterador que está apontando no início.
Phil1970
11
@PeterMortensen Acho que sua edição mudou o significado da última frase ("Do que tenho certeza -> não sei por que"), você poderia verificar?
Fabio diz Reinstate Monica
@ PeterMortensen: O último parágrafo que você editou se tornou um pouco menos legível.
Itachi Uchiwa

Respostas:

32

Ponteiros para elementos de matrizes podem apontar para um elemento válido ou um após o final. Se você incrementa um ponteiro de uma maneira que ultrapassa o final do final, o comportamento é indefinido.

Para sua matriz de tamanho 0, pjá está apontando um além do final, portanto, incrementar isso não é permitido.

Veja C ++ 17 8.7 / 4 sobre o +operador ( ++tem as mesmas restrições):

f a expressão Paponta para o elemento x[i]de um objeto de matriz xcom n elementos, as expressões P + Je J + P(onde Jtem o valor j) apontam para o elemento (possivelmente hipotético) x[i+j]se 0≤i + j≤n; caso contrário, o comportamento é indefinido.

interjay
fonte
2
Portanto, o único caso x[i]é o mesmo de x[i + j]quando ambos ie jtem o valor 0?
Rami Yen
8
@RamiYen x[i]é o mesmo elemento que x[i+j]se j==0.
interjay
11
Ugh, eu odeio a "zona crepuscular" da semântica em C ++ ... +1.
einpoklum
4
@ einpoklum-reinstateMonica: Na verdade, não há zona crepuscular. É apenas C ++ ser consistente, mesmo para o caso N = 0. Para uma matriz de N elementos, há N + 1 valores válidos de ponteiro porque você pode apontar para trás da matriz. Isso significa que você pode começar no início da matriz e incrementar o ponteiro N vezes para chegar ao fim.
MSalters
11
@MaximEgorushkin Minha resposta é sobre o que o idioma atualmente permite. A discussão sobre o que você deseja permitir é fora de tópico.
interjay
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 o próprio array 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.

Raindrop7
fonte
-4

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] 3faz 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_tcostas 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.

Damon
fonte
11
Sua lógica é falha. "Então você não precisa de um objeto?" interpreta mal o Padrão, concentrando-se em uma única regra. Essa regra é sobre o tempo de compilação, se o seu programa é bem formado. Há outra regra sobre o tempo de execução. Somente em tempo de execução você pode realmente falar sobre a existência de objetos em um determinado endereço. seu programa precisa atender a todas as regras; as regras de tempo de compilação no tempo de compilação e as regras de tempo de execução no tempo de execução.
MSalters
5
Você tem falhas de lógica semelhantes com "OK, quem se importa! Contanto que um ponteiro aponte em algum lugar da memória, eu estou bem?". Não. Você precisa seguir todas as regras. A linguagem difícil sobre "fim de uma matriz sendo iniciada em outra matriz" apenas concede à implementação permissão para alocar memória de forma contígua; não precisa manter espaço livre entre alocações. Isso significa que seu código pode ter o mesmo valor A, tanto no final de um objeto de matriz quanto no início de outro.
MSalters
11
"Uma armadilha" não é algo que possa ser descrito pelo comportamento "definido pela implementação". Observe que o interjay encontrou a restrição no +operador (a partir da qual ++flui), o que significa que apontar após "um após o fim" é indefinido.
Martin Bonner suporta Monica
11
@ PeterCordes: Por favor, leia basic.stc, parágrafo 4 . Ele diz "Comportamento indefinido [...] indireto. Qualquer outro uso de um valor de ponteiro inválido tem comportamento definido pela implementação " . Não estou confundindo as pessoas usando esse termo para outro significado. É o texto exato. Não é um comportamento indefinido.
Damon
2
É quase impossível que você tenha encontrado uma brecha para o pós-incremento, mas não cite a seção completa sobre o que o pós-incremento faz. Eu não vou investigar isso agora. Concordou que, se houver, não é intencional. De qualquer forma, por mais legal que fosse, se o ISO C ++ definisse mais coisas para os modelos de memória plana, @MaximEgorushkin, existem outros motivos (como o uso de ponteiros) para não permitir coisas arbitrárias. Ver comentários em As comparações de ponteiros devem ser assinadas ou não assinadas no x86 de 64 bits?
Peter Cordes