L'incremento di un puntatore a un array dinamico di dimensioni 0 non è definito?


34

AFAIK, anche se non possiamo creare un array di memoria statica di dimensioni 0, ma possiamo farlo con quelli dinamici:

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

Come ho letto, si pcomporta come un elemento a un passo. Posso stampare l'indirizzo a cui ppunta.

if(p)
    cout << p << endl;
  • Anche se sono sicuro di non poter dereferenziare quel puntatore (past-last-element) come non possiamo con iteratori (past-last element), ma ciò di cui non sono sicuro è se incrementare quel puntatore p? Un comportamento indefinito (UB) è simile agli iteratori?

    p++; // UB?

4
UB "... Qualsiasi altra situazione (vale a dire, tenta di generare un puntatore che non punta a un elemento dello stesso array o uno oltre la fine) invoca un comportamento indefinito ...." da: en.cppreference.com / w / cpp / language / operator_arithmetic
Richard Critten,

3
Bene, questo è simile a un std::vectoroggetto con 0 in esso. begin()è già uguale a end()quindi non è possibile incrementare un iteratore che punta all'inizio.
Phil1970,

1
@PeterMortensen Penso che la tua modifica abbia cambiato il significato dell'ultima frase ("Di cosa sono sicuro -> Non sono sicuro del perché"), potresti ricontrollare?
Fabio dice di reintegrare Monica il

@PeterMortensen: l'ultimo paragrafo che hai modificato è diventato un po 'meno leggibile.
Itachi Uchiwa,

Risposte:


32

I puntatori agli elementi delle matrici possono indicare un elemento valido o uno oltre la fine. Se si incrementa un puntatore in un modo che supera di oltre uno alla fine, il comportamento non è definito.

Per il tuo array di dimensioni 0, psta già indicando uno oltre la fine, quindi non è consentito incrementarlo.

Vedi C ++ 17 8.7 / 4 per quanto riguarda l' +operatore ( ++ha le stesse restrizioni):

f l'espressione Ppunta all'elemento x[i]di un oggetto array xcon n elementi, le espressioni P + Je J + P(dove Jha il valore j) puntano all'elemento (possibilmente ipotetico) x[i+j]se 0≤i + j≤n; in caso contrario, il comportamento non è definito.


2
Quindi l'unico caso x[i]è lo stesso di x[i + j]quando entrambi ie jhanno il valore 0?
Rami Yen,

8
@RamiYen x[i]è lo stesso elemento di x[i+j]se j==0.
interjay

1
Uffa, odio la "zona crepuscolare" della semantica C ++ ... +1 però.
einpoklum,

4
@ einpoklum-reinstateMonica: Non esiste davvero una zona crepuscolare. È solo C ++ che è coerente anche per il caso N = 0. Per una matrice di N elementi, ci sono valori di puntatore N + 1 validi perché è possibile puntare dietro la matrice. Ciò significa che è possibile iniziare all'inizio dell'array e aumentare il puntatore N volte per arrivare alla fine.
MSalters il

1
@MaximEgorushkin La mia risposta riguarda ciò che la lingua attualmente consente. La discussione su di te che vorrebbe consentire invece è fuori tema.
Interjay

2

Immagino tu abbia già la risposta; Se guardi un po 'più in profondità: hai detto che l'incremento di un iteratore off-end è UB quindi: questa risposta è in che cosa è un iteratore?

L'iteratore è solo un oggetto che ha un puntatore e incrementare quell'iteratore sta realmente incrementando il puntatore che ha. Pertanto in molti aspetti un iteratore viene gestito in termini di puntatore.

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

int * p = arr; // p indica il primo elemento in arr

++ p; // p indica arr [1]

Proprio come possiamo usare gli iteratori per attraversare gli elementi in un vettore, possiamo usare i puntatori per attraversare gli elementi in un array. Naturalmente, per fare ciò, dobbiamo ottenere puntatori al primo e uno oltre l'ultimo elemento. Come abbiamo appena visto, possiamo ottenere un puntatore al primo elemento usando l'array stesso o prendendo l'indirizzo del primo elemento. È possibile ottenere un puntatore predefinito utilizzando un'altra proprietà speciale degli array. Possiamo prendere l'indirizzo dell'elemento inesistente uno oltre l'ultimo elemento di un array:

int * e = & arr [10]; // puntatore appena passato l'ultimo elemento in arr

Qui abbiamo usato l'operatore subscript per indicizzare un elemento inesistente; arr ha dieci elementi, quindi l'ultimo elemento in arr è nella posizione di indice 9. L'unica cosa che possiamo fare con questo elemento è prendere il suo indirizzo, che facciamo per inizializzare e. Come un iteratore off-the-end (§ 3.4.1, p. 106), un puntatore off-the-end non punta a un elemento. Di conseguenza, non possiamo dereferenziare o incrementare un puntatore predefinito.

Questo è tratto dall'edizione C ++ primer 5 di Lipmann.

Quindi è UB non farlo.


-4

Nel senso più stretto, questo non è un comportamento indefinito, ma definito dall'implementazione. Quindi, anche se sconsigliabile se si prevede di supportare architetture non tradizionali, è possibile farlo.

La citazione standard data da interjay è buona, indicando UB, ma è solo il secondo miglior riscontro secondo me, dal momento che si occupa dell'aritmetica puntatore-puntatore (stranamente, uno è esplicitamente UB, mentre l'altro no). C'è un paragrafo che tratta direttamente l'operazione nella domanda:

[expr.post.incr] / [expr.pre.incr]
L'operando deve essere [...] o un puntatore a un tipo di oggetto completamente definito.

Oh, aspetta un momento, un tipo di oggetto completamente definito? È tutto? Voglio dire, davvero, tipo ? Quindi non hai bisogno di un oggetto?
Ci vuole un bel po 'di lettura per trovare effettivamente un indizio che qualcosa dentro potrebbe non essere così ben definito. Perché finora, sembra che tu sia perfettamente autorizzato a farlo, senza restrizioni.

[basic.compound] 3fa una dichiarazione su quale tipo di puntatore si può avere e, essendo nessuno degli altri tre, il risultato dell'operazione sarebbe chiaramente inferiore a 3.4: puntatore non valido .
Tuttavia non dice che non ti è permesso avere un puntatore non valido. Al contrario, elenca alcune condizioni normali molto comuni (ad es. Fine della durata della memorizzazione) in cui i puntatori diventano regolarmente non validi. Quindi sembra che ciò accada. E senza dubbio:

[basic.stc] 4
L'indirizzamento attraverso un valore di puntatore non valido e il passaggio di un valore di puntatore non valido a una funzione di deallocazione hanno un comportamento indefinito. Qualsiasi altro uso di un valore di puntatore non valido ha un comportamento definito dall'implementazione.

Stiamo facendo un "qualsiasi altro" lì, quindi non è un comportamento indefinito, ma definito dall'implementazione, quindi generalmente ammissibile (a meno che l'implementazione non dica esplicitamente qualcosa di diverso).

Sfortunatamente, non è la fine della storia. Sebbene il risultato netto non cambi più da qui in poi, diventa più confuso, più a lungo si cerca "puntatore":

[basic.compound]
Un valore valido di un tipo di puntatore oggetto rappresenta l' indirizzo di un byte in memoria o un puntatore nullo. Se un oggetto di tipo T si trova su un indirizzo [...] A si dice che punti a quell'oggetto, indipendentemente da come è stato ottenuto il valore .
[Nota: ad esempio, l'indirizzo oltre la fine di un array verrebbe considerato come riferimento a un oggetto non correlato del tipo di elemento dell'array che potrebbe trovarsi a quell'indirizzo. [...]].

Leggi come: OK, chi se ne frega! Finché un puntatore punta da qualche parte nella memoria , sto bene?

[basic.stc.dynamic.safety] Un valore di puntatore è un puntatore derivato in modo sicuro [blah blah]

Leggi come: OK, derivato in sicurezza, qualunque cosa. Non spiega di cosa si tratta, né dice che ne ho davvero bisogno. Safely-derivati-the-diamine. Apparentemente posso ancora avere puntatori non derivati ​​in modo sicuro. Immagino che dereferenziarli non sarebbe probabilmente una buona idea, ma è perfettamente ammissibile averli. Non dice diversamente.

Un'implementazione può avere una minore sicurezza del puntatore, nel qual caso la validità di un valore di puntatore non dipende dal fatto che si tratti di un valore di puntatore derivato in modo sicuro.

Oh, quindi potrebbe non importare, proprio quello che pensavo. Ma aspetta ... "non può"? Ciò significa che potrebbe anche . Come lo so?

In alternativa, un'implementazione può avere una rigorosa sicurezza del puntatore, nel qual caso un valore del puntatore che non è un valore del puntatore derivato in modo sicuro è un valore del puntatore non valido a meno che l'oggetto completo di riferimento abbia una durata di archiviazione dinamica e sia stato precedentemente dichiarato raggiungibile

Aspetta, quindi è anche possibile che abbia bisogno di chiamare declare_reachable()ogni puntatore? Come lo so?

Ora puoi convertirti in intptr_t, che è ben definito, dando una rappresentazione intera di un puntatore derivato in modo sicuro. Per cui, ovviamente, essendo un numero intero, è perfettamente legittimo e ben definito incrementarlo a piacere.
E sì, puoi convertire la parte intptr_tposteriore in un puntatore, che è anche ben definito. Solo, non essendo il valore originale, non è più garantito che tu abbia un puntatore derivato in modo sicuro (ovviamente). Tuttavia, tutto sommato, alla lettera dello standard, pur essendo definito dall'implementazione, questa è una cosa legittima al 100% da fare:

[expr.reinterpret.cast] 5
Un valore di tipo integrale o di tipo di enumerazione può essere esplicitamente convertito in un puntatore. Un puntatore convertito in un numero intero di dimensioni sufficienti [...] e di nuovo nello stesso tipo di puntatore [...] valore originale; i mapping tra puntatori e numeri interi sono altrimenti definiti dall'implementazione.

La presa

I puntatori sono solo numeri normali, solo tu li usi come puntatori. Oh, se solo fosse vero!
Sfortunatamente, esistono architetture in cui ciò non è affatto vero, e la semplice generazione di un puntatore non valido (non dereferenziarlo, semplicemente averlo in un registro puntatori) causerà una trappola.

Quindi questa è la base dell '"implementazione definita". Ciò e il fatto che l'incremento di un puntatore ogni volta che lo desideri, come ti pare, potrebbe ovviamente causare un overflow, che lo standard non vuole affrontare. La fine dello spazio degli indirizzi dell'applicazione potrebbe non coincidere con la posizione dell'overflow e non si sa nemmeno se esiste qualcosa come l'overflow per i puntatori su una particolare architettura. Tutto sommato è un disastro da incubo non in alcuna relazione dei possibili benefici.

Affrontare la condizione di un oggetto passato dall'altro lato è semplice: l'implementazione deve semplicemente assicurarsi che nessun oggetto sia mai stato allocato, quindi l'ultimo byte nello spazio degli indirizzi è occupato. Quindi è ben definito in quanto utile e banale da garantire.


1
La tua logica è difettosa. "Quindi non hai bisogno di un oggetto?" fraintende lo standard concentrandosi su una singola regola. Tale regola riguarda i tempi di compilazione, indipendentemente dal fatto che il programma sia ben formato. C'è un'altra regola sul tempo di esecuzione. Solo in fase di esecuzione puoi effettivamente parlare dell'esistenza di oggetti a un determinato indirizzo. il tuo programma deve soddisfare tutte le regole; le regole di compilazione in fase di compilazione e le regole di run-time in fase di run-time.
MSalters

5
Hai difetti logici simili con "OK, a chi importa! Finché un puntatore punta da qualche parte nella memoria, sto bene?". No. Devi seguire tutte le regole. Il linguaggio difficile su "la fine di un array è l'inizio di un altro array" dà solo l' autorizzazione all'implementazione per allocare la memoria in modo contiguo; non è necessario mantenere spazio libero tra le allocazioni. Ciò significa che il tuo codice potrebbe avere lo stesso valore A sia della fine di un oggetto array sia dell'inizio di un altro.
Salterio

1
"Una trappola" non è qualcosa che può essere descritto dal comportamento "definito dall'implementazione". Si noti che l'interjay ha trovato la restrizione sull'operatore +(da cui ++scorre), il che significa che il puntamento dopo "uno dopo l'altro" non è definito.
Martin Bonner supporta Monica il

1
@PeterCordes: leggi basic.stc, paragrafo 4 . Dice "Comportamento indefinito indiretto [...]. Qualsiasi altro uso di un valore di puntatore non valido ha un comportamento definito dall'implementazione " . Non confondo le persone usando quel termine per un altro significato. È la formulazione esatta. Si Non undefined comportamento.
Damon,

2
È appena possibile che tu abbia trovato una lacuna per il post-incremento, ma non citi l'intera sezione su ciò che fa il post-incremento. Non lo esaminerò io stesso ora. Concordato che se ce n'è uno, è involontario. Comunque, bello come sarebbe se ISO C ++ definisse più cose per i modelli di memoria flat, @MaximEgorushkin, ci sono altri motivi (come il wrapper del puntatore) per non consentire cose arbitrarie. Vedi commenti su I confronti dei puntatori devono essere firmati o non firmati in x86 a 64 bit?
Peter Cordes,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.