Un puntatore con l'indirizzo e il tipo corretti è ancora sempre un puntatore valido da C ++ 17?


84

(In riferimento a questa domanda e risposta .)

Prima dello standard C ++ 17, la seguente frase era inclusa in [basic.compound] / 3 :

Se un oggetto di tipo T si trova in un indirizzo A, si dice che un puntatore di tipo cv T * il cui valore è l'indirizzo A punti a quell'oggetto, indipendentemente da come è stato ottenuto il valore.

Ma da C ++ 17 questa frase è stata rimossa .

Ad esempio, credo che questa frase abbia reso questo codice di esempio definito e che da C ++ 17 questo sia un comportamento indefinito:

 alignas(int) unsigned char buffer[2*sizeof(int)];
 auto p1=new(buffer) int{};
 auto p2=new(p1+1) int{};
 *(p1+1)=10;

Prima di C ++ 17, p1+1contiene l'indirizzo *p2e ha il tipo corretto, così *(p1+1)è un puntatore a *p2. In C ++ 17 p1+1è un puntatore oltre la fine , quindi non è un puntatore a un oggetto e credo che non sia dereferenziabile.

Questa è l'interpretazione di questa modifica del diritto standard o ci sono altre regole che compensano la cancellazione della frase citata?


Nota: ci sono regole nuove / aggiornate sulla provenienza dei puntatori in [basic.stc.dynamic.safety] e [util.dynamic.safety]
MM

@MM Ciò è importante solo per le implementazioni con una rigida sicurezza del puntatore, che è un insieme vuoto (entro l'errore sperimentale).
TC

4
L'affermazione citata non è mai stata effettivamente vera nella pratica. Dato int a, b = 0;, non puoi farlo *(&a + 1) = 1;anche se hai controllato &a + 1 == &b. Se è possibile ottenere un puntatore valido a un oggetto solo indovinando il suo indirizzo, anche la memorizzazione delle variabili locali nei registri diventa problematica.
TC

@TC 1) Quale compilatore mette una var in reg dopo aver preso il suo indirizzo? 2) Come indovinare correttamente un indirizzo senza misurarlo?
curioso

@curiousguy Esattamente questo è il motivo per cui semplicemente lanciare un numero ottenuto con altri mezzi (ad esempio, indovinare) all'indirizzo in cui si trova un oggetto è problematico: crea un alias di quell'oggetto ma il compilatore non ne è consapevole. Se, al contrario, prendi l'indirizzo dell'oggetto è come dici: il compilatore viene avvisato e si sincronizza di conseguenza.
Peter - Ripristina Monica il

Risposte:


45

Questa interpretazione di questa modifica del diritto standard o ci sono altre regole che compensano la cancellazione di questa frase citata?

Sì, questa interpretazione è corretta. Un puntatore oltre la fine non è semplicemente convertibile in un altro valore del puntatore che capita di puntare a quell'indirizzo.

Il nuovo [basic.compound] / 3 dice:

Ogni valore del tipo di puntatore è uno dei seguenti:
(3.1) un puntatore a un oggetto o una funzione (si dice che il puntatore punta all'oggetto o alla funzione), o
(3.2) un puntatore oltre la fine di un oggetto ([espr .add]) o

Quelle si escludono a vicenda. p1+1è un puntatore oltre la fine, non un puntatore a un oggetto. p1+1punta a un ipotetico x[1]array di dimensione 1 a p1, non a p2. Questi due oggetti non sono interconvertibili con il puntatore.

Abbiamo anche la nota non normativa:

[Nota: un puntatore oltre la fine di un oggetto ([expr.add]) non è considerato puntare a un oggetto non correlato del tipo di oggetto che potrebbe trovarsi a quell'indirizzo. [...]

che chiarisce l'intento.


Come sottolinea TC in numerosi commenti (in particolare questo ), questo è davvero un caso speciale del problema che deriva dal tentativo di implementare std::vector, che [v.data(), v.data() + v.size())deve essere un intervallo valido e tuttavia vectornon crea un oggetto array, quindi il solo l'aritmetica del puntatore definita andrebbe da un dato oggetto nel vettore a oltre la fine del suo ipotetico array di una dimensione. Per ulteriori risorse, vedere CWG 2182 , questa discussione standard e due revisioni di un documento sull'argomento: P0593R0 e P0593R1 (sezione 1.3 in particolare).


3
Questo esempio è fondamentalmente un caso speciale del noto " vectorproblema di implementabilità". +1.
TC

2
@Oliv Il caso generale esiste da C ++ 03. La causa principale è che l'aritmetica del puntatore non funziona come previsto perché non si dispone di un oggetto array.
TC

1
@TC Credevo che l'unico problema derivasse dalla restrizione sull'aritmetica del puntatore. Questa cancellazione della frase non aggiunge un nuovo problema? L'esempio di codice è anche UB in pre-C ++ 17?
Oliv

1
@Oliv Se l'aritmetica del puntatore è fissa, il tuo p1+1non produrrà più un puntatore oltre la fine e l'intera discussione sui puntatori oltre la fine è discutibile. Il tuo caso speciale a due elementi potrebbe non essere UB pre-17, ma non è nemmeno molto interessante.
TC

5
@TC Puoi indicarmi da qualche parte che posso leggere su questo "problema di implementabilità vettoriale"?
SirGuy

8

Nel tuo esempio, *(p1 + 1) = 10;dovrebbe essere UB, perché lo è uno oltre la fine dell'array di dimensione 1. Ma qui ci troviamo in un caso molto speciale, perché l'array è stato costruito dinamicamente in un array di caratteri più grande.

La creazione di oggetti dinamici è descritta in 4.5 Il modello di oggetti C ++ [intro.object] , §3 della bozza n4659 dello standard C ++:

3 Se viene creato un oggetto completo (8.3.4) nella memoria associata a un altro oggetto e di tipo "matrice di N caratteri senza segno" o di tipo "matrice di N std :: byte" (21.2.1), tale matrice fornisce per l'oggetto creato se:
(3.1) - la durata di e è iniziata e non terminata, e
(3.2) - la memoria per il nuovo oggetto rientra interamente in e, e
(3.3) - non esiste un oggetto array più piccolo che soddisfi questi vincoli.

Il 3.3 sembra piuttosto poco chiaro, ma gli esempi seguenti rendono più chiaro l'intento:

struct A { unsigned char a[32]; };
struct B { unsigned char b[16]; };
A a;
B *b = new (a.a + 8) B; // a.a provides storage for *b
int *p = new (b->b + 4) int; // b->b provides storage for *p
// a.a does not provide storage for *p (directly),
// but *p is nested within a (see below)

Quindi, nell'esempio, l' bufferarray fornisce spazio di archiviazione per entrambi *p1e *p2.

I paragrafi seguenti dimostrano che l'oggetto completo per entrambi *p1e *p2èbuffer :

4 Un oggetto a è annidato all'interno di un altro oggetto b se:
(4.1) - a è un sottooggetto di b, o
(4.2) - b fornisce memoria per a, o
(4.3) - esiste un oggetto c dove a è annidato all'interno di c e c è annidato in b.

5 Per ogni oggetto x, c'è un oggetto chiamato oggetto completo di x, determinato come segue:
(5.1) - Se x è un oggetto completo, allora l'oggetto completo di x è esso stesso.
(5.2) - Altrimenti, l'oggetto completo di x è l'oggetto completo dell'oggetto (unico) che contiene x.

Una volta stabilito ciò, l'altra parte rilevante della bozza n4659 per C ++ 17 è [basic.coumpound] §3 (enfatizza la mia):

3 ... Ogni valore del tipo di puntatore è uno dei seguenti:
(3.1) - un puntatore a un oggetto o una funzione (si dice che il puntatore punti all'oggetto o alla funzione), o
(3.2) - un puntatore oltre la fine di un oggetto (8.7), o
(3.3) - il valore del puntatore nullo (7.11) per quel tipo, o
(3.4) - un valore del puntatore non valido.

Un valore di un tipo di puntatore che è un puntatore alla o oltre la fine di un oggetto rappresenta l'indirizzo del primo byte in memoria (4.4) occupato dall'oggetto o il primo byte in memoria dopo la fine della memoria occupata dall'oggetto , rispettivamente. [Nota: un puntatore oltre la fine di un oggetto (8.7) non è considerato puntare a un non correlatooggetto del tipo di oggetto che potrebbe trovarsi a quell'indirizzo. Il valore di un puntatore diventa non valido quando la memoria che indica raggiunge la fine della sua durata di memorizzazione; vedere 6.7. —End note] Ai fini dell'aritmetica dei puntatori (8.7) e del confronto (8.9, 8.10), un puntatore oltre la fine dell'ultimo elemento di un array x di n elementi è considerato equivalente a un puntatore a un ipotetico elemento x [ n]. La rappresentazione del valore dei tipi di puntatore è definita dall'implementazione. I puntatori a tipi compatibili con il layout devono avere gli stessi requisiti di rappresentazione del valore e allineamento (6.11) ...

La nota Un puntatore oltre la fine ... non si applica qui perché gli oggetti puntati da p1e p2non sono estranei , ma sono annidati nello stesso oggetto completo, quindi l'aritmetica dei puntatori ha senso all'interno dell'oggetto che fornisce memoria: p2 - p1è definito ed è (&buffer[sizeof(int)] - buffer]) / sizeof(int)questo è 1.

Quindi p1 + 1 è un puntatore a *p2, *(p1 + 1) = 10;ha un comportamento definito e imposta il valore di *p2.


Ho anche letto l'allegato C4 sulla compatibilità tra C ++ 14 e gli standard attuali (C ++ 17). Rimuovere la possibilità di utilizzare l'aritmetica del puntatore tra oggetti creati dinamicamente in un singolo array di caratteri sarebbe un cambiamento importante che IMHO dovrebbe essere citato lì, perché è una caratteristica comunemente usata. Poiché non esiste nulla al riguardo nelle pagine di compatibilità, penso che confermi che non era l'intento dello standard vietarlo.

In particolare, sconfiggerebbe quella comune costruzione dinamica di un array di oggetti da una classe senza costruttore predefinito:

class T {
    ...
    public T(U initialization) {
        ...
    }
};
...
unsigned char *mem = new unsigned char[N * sizeof(T)];
T * arr = reinterpret_cast<T*>(mem); // See the array as an array of N T
for (i=0; i<N; i++) {
    U u(...);
    new(arr + i) T(u);
}

arr può quindi essere utilizzato come puntatore al primo elemento di un array ...


Aha, quindi il mondo non è impazzito. +1
StoryTeller - Unslander Monica

@StoryTeller: lo spero anch'io. Inoltre non una parola al riguardo nella sezione compatibilità. Ma sembra che l'opinione opposta abbia più reputazione qui ...
Serge Ballesta

2
Stai afferrando una sola parola, "non correlata", in una nota non normativa, e dandole un significato che non può sopportare, in contraddizione con le regole normative in [expr.add] che governano l'aritmetica dei puntatori. Non c'è niente nell'Allegato C perché l'aritmetica del puntatore del caso generale non ha mai funzionato in nessuno standard. Non c'è niente da rompere.
TC

3
@TC: Google è molto inutile nell'individuare qualsiasi informazione su questo "problema di implementabilità dei vettori", potresti aiutarci?
Matthieu M.

6
@MatthieuM. Vedere il problema principale 2182 , questo thread di discussione std, P0593R0 e P0593R1 (in particolare la sezione 1.3) . Il problema di base è che vectornon crea (e non può) creare un oggetto array, ma ha un'interfaccia che consente all'utente di ottenere un puntatore che supporta l'aritmetica dei puntatori (che è definita solo per i puntatori in oggetti array).
TC

1

Per espandere le risposte fornite qui è un esempio di ciò che credo che la formulazione rivista escluda:

Avvertenza: comportamento indefinito

#include <iostream>
int main() {
    int A[1]{7};
    int B[1]{10};
    bool same{(B)==(A+1)};

    std::cout<<B<< ' '<< A <<' '<<sizeof(*A)<<'\n';
    std::cout<<(same?"same":"not same")<<'\n';
    std::cout<<*(A+1)<<'\n';//!!!!!  
    return 0;
}

Per ragioni interamente dipendenti (e fragili) dall'attuazione, il possibile risultato di questo programma è:

0x7fff1e4f2a64 0x7fff1e4f2a60 4
same
10

Quell'output mostra che i due array (in quel caso) sono immagazzinati in memoria in modo tale che "uno oltre la fine" di Acontenga il valore dell'indirizzo del primo elemento di B.

La specifica rivista garantisce che a prescindere A+1non sia mai un valido puntatore a B. La vecchia frase "indipendentemente da come si ottiene il valore" dice che se "A + 1" punta a "B [0]", allora è un puntatore valido a "B [0]". Non può essere buono e sicuramente non è mai l'intenzione.


Ciò impedisce anche in modo efficace l'uso di un array vuoto alla fine di una struttura in modo tale che una classe derivata o un allocatore personalizzato new possa specificare un array di dimensioni personalizzate? Forse il nuovo problema riguarda il "indipendentemente da come": ci sono alcuni modi validi e altri pericolosi?
Gem Taylor

@ Persixty Quindi il valore di un oggetto puntatore è determinato dai byte degli oggetti e nient'altro. Quindi due oggetti con lo stesso stato puntano allo stesso oggetto. Se uno è valido, lo è anche l'altro. Quindi su architetture comuni, dove un valore di puntatore è rappresentato come un numero, due puntatori con valori uguali puntano agli stessi oggetti e uno all'estremità gli stessi altri oggetti.
curioso il

@ Persixty Inoltre, il tipo banale significa che puoi enumerare i possibili valori di un tipo. In sostanza, qualsiasi compilatore moderno in qualsiasi modalità di ottimizzazione (anche -O0su alcuni compilatori) non considera i puntatori come tipi banali. I compilatori non trattano seriamente i requisiti dello std, e nemmeno le persone che scrivono lo std, che sognano un linguaggio diverso e fanno tutti i tipi di invenzioni che contraddicono direttamente i principi di base. Ovviamente gli utenti sono confusi e talvolta trattati male quando si lamentano di bug del compilatore.
curioso

La nota non normativa nella domanda vuole che pensiamo a "uno-oltre-la-fine" come se non indicasse nulla. Sappiamo entrambi che in pratica potrebbe indicare qualcosa e in pratica potrebbe essere possibile dereferenziarlo. Ma quello (secondo lo standard) non è un programma valido. Possiamo immaginare un'implementazione che sa che un puntatore è stato ottenuto con l'aritmetica al passato e solleva un'eccezione se dereferenziato. Anche se conosco la piattaforma che lo fa. Penso che lo standard non voglia escluderlo.
Persixty

@curiousguy Inoltre, non sono sicuro di cosa intendi enumerando i possibili valori. Non è una caratteristica richiesta di un tipo banale come definito da C ++.
Persixty il
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.