L’inizializzazione comporta la conversione da lvalue a rvalue? È `int x = x;` UB?

Lo standard C ++ contiene un esempio semi-famoso di ricerca del nome “sorprendente” in 3.3.2, “Punto di dichiarazione”:

int x = x; 

Questo inizializza x con se stesso, che (essendo un tipo primitivo) non è inizializzato e quindi ha un valore indeterminato (assumendo che sia una variabile automatica).

Questo comportamento è in realtà non definito?

In base alla 4.1 “Conversione da valore nominale a valore nominale”, è un comportamento indefinito eseguire la conversione da valore a valore su un valore non inizializzato. x mano destra subisce questa conversione? In tal caso, l’esempio avrebbe un comportamento indefinito?

AGGIORNAMENTO: Dopo la discussione nei commenti, ho aggiunto ulteriori prove alla fine di questa risposta.


Disclaimer : ammetto che questa risposta è piuttosto speculativa. L’attuale formulazione dello standard C ++ 11, d’altra parte, non sembra consentire una risposta più formale.


Nel contesto di questa domanda e risposta , è emerso che lo standard C ++ 11 non riesce a specificare formalmente quali categorie di valore sono previste da ciascun costrutto linguistico. Di seguito mi concentrerò principalmente sugli operatori integrati , sebbene la domanda riguardi gli inizializzatori . Alla fine, finirò per estendere le conclusioni che ho disegnato per il caso degli operatori al caso degli inizializzatori.

Nel caso di operatori integrati, nonostante la mancanza di una specifica formale, nello standard si trovano prove (non normative) che la specifica prevista è quella di lasciare prevedere i valori preregistrati ovunque sia necessario un valore e quando non specificato altrimenti

Ad esempio, una nota nel paragrafo 3.10 / 1 dice:

La discussione di ciascun operatore integrato nella Clausola 5 indica la categoria del valore che produce e le categorie di valore degli operandi che si aspetta. Ad esempio, gli operatori di assegnazione incorporati si aspettano che l’operando di sinistra sia un lvalue e che l’operando destro sia un valore di prvalore e produca un lvalue come risultato. Gli operatori definiti dall’utente sono funzioni e le categorie di valori che si aspettano e producono sono determinate dai rispettivi parametri e tipi di ritorno

D’altra parte, la sezione 5.17 sugli operatori incaricati dell’assegnazione non ne parla. Tuttavia, la possibilità di eseguire una conversione da lvalue a rvalue è menzionata, sempre in una nota (paragrafo 5.17 / 1):

Pertanto, una chiamata di funzione non deve intervenire tra la conversione da valore a valore e l’effetto collaterale associato a un singolo operatore di assegnazione composto

Naturalmente, se non ci si aspettasse alcun valore, questa nota sarebbe priva di significato.

Un’altra prova si trova in 4/8, come indicato da Johannes Schaub nei commenti alle domande e risposte collegate:

Ci sono alcuni contesti in cui alcune conversioni vengono soppresse. Ad esempio, la conversione lvalue-rvalue non viene eseguita sull’operando di unario e operatore. Eccezioni specifiche sono fornite nelle descrizioni di tali operatori e contesti.

Ciò sembra implicare che la conversione da lvalue a valore viene eseguita su tutti gli operandi degli operatori integrati, tranne quando specificato diversamente. Ciò significherebbe, a sua volta, che i rvalues ​​sono previsti come operandi di operatori integrati se non diversamente specificato.


CONGETTURARE:

Anche se l’inizializzazione non è assegnata, e quindi gli operatori non entrano nella discussione, il mio sospetto è che quest’area della specifica sia influenzata dallo stesso problema descritto sopra.

Tracce a supporto di questa convinzione possono essere trovate anche nel Paragrafo 8.5.2 / 5, riguardo all’inizializzazione dei riferimenti (per i quali non è necessario il valore dell’espressione di inizializzatore lvalue):

Le solite conversioni standard da lvalue a rvalue (4.1), array-to-pointer (4.2) e function-to-pointer (4.3) non sono necessarie, e quindi sono soppresse, quando vengono eseguiti tali collegamenti diretti ai lvalue.

La parola “solito” sembra implicare che quando si inizializzano oggetti che non sono di un tipo di riferimento, si intende applicare la conversione da lvalue a rvalue.

Pertanto, ritengo che, sebbene i requisiti sulla categoria di valori attesi degli inizializzatori siano mal definiti (se non del tutto mancanti), sulla base delle prove fornite ha senso presumere che la specifica prevista sia quella:

Ovunque sia richiesto un valore da un costrutto linguistico, è previsto un valore di prenatale se non diversamente specificato .

In base a questo presupposto, nel tuo esempio sarebbe necessaria una conversione da lvalue a valore e ciò porterebbe a un comportamento non definito.


ULTERIORI PROVE:

Giusto per fornire ulteriori prove a supporto di questa congettura, supponiamo che sia sbagliato , in modo che nessuna conversione da lvalue a valore sia effettivamente necessaria per l’inizializzazione della copia e considerare il seguente codice (grazie a jogojapan per il contributo):

 int y; int x = y; // No UB short t; int u = t; // UB! (Do not like this non-uniformity, but could accept it) int z; z = x; // No UB (x is not uninitialized) z = y; // UB! (Assuming assignment operators expect a prvalue, see above) // This would be very counterintuitive, since x == y 

Questo comportamento non uniforms non ha molto senso per me. Ciò che ha più senso per IMO è che, ovunque sia richiesto un valore, è previsto un valore nominale.

Inoltre, come giustamente fa notare Jesse Good nella sua risposta, il paragrafo chiave dello standard C ++ è 8.5 / 16:

– Altrimenti, il valore iniziale dell’object inizializzato è il valore (eventualmente convertito) dell’espressione di inizializzazione . Verranno utilizzate le conversioni standard (clausola 4), se necessario , per convertire l’espressione dell’inizializzatore nella versione cv-non qualificata del tipo di destinazione; non vengono prese in considerazione conversioni definite dall’utente. Se la conversione non può essere eseguita, l’inizializzazione è mal formata. [Nota: un’espressione di tipo “cv1 T” può inizializzare un object di tipo “cv2 T” indipendentemente dai qualificatori cv cv1 e cv2.

Tuttavia, mentre Jesse si concentra principalmente sul bit ” se necessario “, vorrei anche sottolineare la parola ” tipo “. Il paragrafo precedente menziona che le conversioni standard saranno utilizzate ” se necessario ” per convertire il tipo di destinazione, ma non dice nulla sulle conversioni di categorie :

  1. Le conversioni di categoria verranno eseguite se necessario?
  2. Sono necessari?

Per quanto riguarda la seconda domanda, come discusso nella parte originale della risposta, lo standard C ++ 11 attualmente non specifica se le conversioni di categoria sono necessarie o meno, perché da nessuna parte viene menzionato se l’inizializzazione della copia prevede un valore di inizializzazione come inizializzatore . Quindi, una risposta chiara è imansible da dare. Tuttavia, credo di aver fornito prove sufficienti per ritenere che questa fosse la specifica prevista , in modo che la risposta fosse “Sì”.

Per quanto riguarda la prima domanda, mi sembra ragionevole che la risposta sia “Sì”. Se fosse “No”, ovviamente i programmi corretti sarebbero mal formati:

 int y = 0; int x = y; // y is lvalue, prvalue expected (assuming the conjecture is correct) 

Per riassumere (A1 = ” Risposta alla domanda 1 “, A2 = ” Risposta alla domanda 2 “):

  | A2 = Yes | A2 = No | ---------|------------|---------| A1 = Yes | UB | No UB | A1 = No | ill-formsd | No UB | --------------------------------- 

Se A2 è “No”, A1 non ha importanza: non c’è UB, ma le bizzarre situazioni del primo esempio (es. z = y dà UB, ma non z = x anche se x == y ) appaiono. Se A2 è “Sì”, d’altra parte, A1 diventa cruciale; tuttavia, sono state fornite prove sufficienti per dimostrare che sarebbe “Sì”.

Pertanto, la mia tesi è che A1 = “Sì” e A2 = “Sì”, e dovremmo avere un comportamento indefinito .


ULTERIORE EVIDENZA:

Questo rapporto sui difetti (per gentile concessione di Jesse Good ) propone un cambiamento finalizzato a dare un comportamento indefinito in questo caso:

[…] Inoltre, il paragrafo 1 [conv.lval] 4.1 afferma che l’applicazione della conversione da valore a valore a un “object [che] non è inizializzato” comporta un comportamento non definito; questo dovrebbe essere riformulato in termini di un object con un valore indeterminato .

In particolare, la formulazione proposta per il paragrafo 4.1 dice:

Quando si verifica una conversione da lvalue a rvalue in un operando non valutato o in una sottoespressione (clausola 5 [expr]) non si accede al valore contenuto nell’object referenziato. In tutti gli altri casi, il risultato della conversione viene determinato in base alle seguenti regole:

– Se T è (possibilmente qualificato in cv) std :: nullptr_t, il risultato è una costante di puntatore nullo (4.10 [conv.ptr]).

– Altrimenti, se il glvalue T ha un tipo di class, la copia di conversione-inizializza un temporaneo di tipo T dal glvalue e il risultato della conversione è un valore per il temporaneo.

– Altrimenti, se l’object a cui si riferisce il glvalue contiene un valore di puntatore non valido (3.7.4.2 [basic.stc.dynamic.deallocation], 3.7.4.3 [basic.stc.dynamic.safety]), il comportamento è definito dall’implementazione .

– Altrimenti, se T è un tipo di carattere non firmato (eventualmente qualificato per cv) (3.9.1 [basic.fundamental]), e l’object a cui si riferisce il glvalue contiene un valore indeterminato (5.3.4 [expr.new], 8.5 [dcl.init], 12.6.2 [class.base.init]), e quell’object non ha durata di memorizzazione automatica o il glvalue era l’operando di un unario e operatore o era associato a un riferimento, il risultato è un valore non specificato. [Nota a piè di pagina: il valore può essere diverso ogni volta che la conversione lvalue-valore viene applicata all’object. Un object char senza segno con valore indeterminato assegnato a un registro potrebbe trap. -End nota a piè di pagina]

Altrimenti, se l’object a cui si riferisce il glvalue contiene un valore indeterminato, il comportamento non è definito.

– Altrimenti, se glValue ha (possibilmente qualificato cv) il tipo std :: nullptr_t, il risultato del prvalue è una costante del puntatore nullo (4.10 [conv.ptr]). Altrimenti, il valore contenuto nell’object indicato da glvalue è il risultato del prvalore.

Una sequenza di conversione implicita di un’espressione e di tipo T è definita equivalente alla seguente dichiarazione, utilizzando t come risultato della conversione (categoria valore modulo, che sarà definita in base a T ), 4p3 e 4p6

 T t = e; 

L’effetto di ogni conversione implicita è lo stesso di eseguire la dichiarazione e l’inizializzazione corrispondenti e quindi di utilizzare la variabile temporanea come risultato della conversione.

Nella clausola 4, la conversione di un’espressione in un tipo produce sempre espressioni con una proprietà specifica. Ad esempio, la conversione di 0 in int* produce un valore di puntatore nullo e non un solo valore di puntatore arbitrario. Anche la categoria valore è una proprietà specifica di un’espressione e il suo risultato è definito come segue

Il risultato è un lvalue se T è un tipo di riferimento di lvalue o un riferimento di rvalue al tipo di funzione (8.3.2), un valore x se T è un riferimento di rvalue al tipo di object e un valore diverso in caso contrario.

Quindi sappiamo che in int t = e; , il risultato della sequenza di conversione è un valore di prvalore, perché int è un tipo non di riferimento. Quindi, se forniamo una glVue, abbiamo evidentemente bisogno di una conversione. 3.10p2 chiarisce ulteriormente che non lasciare dubbi

Ogni volta che un glvalue appare in un contesto in cui è previsto un valore nominale, il glvalue viene convertito in un valore di prvalore; vedere 4.1, 4.2 e 4.3.

questo non è un comportamento indefinito. Semplicemente non conosci i suoi valori specifici, perché non c’è alcuna inizializzazione. Se la variabile è globale e di tipo built-in, il compilatore la metterà inizializzata al valore corretto. Se la variabile è locale, il compilatore non la inizializza, quindi tutte le variabili sono inizializzate su se stessi, non fare affidamento sul compilatore.

Il comportamento non è indefinito. La variabile non è inizializzata e rimane con qualsiasi valore di avvio non inizializzato del valore casuale. Un esempio della prova di clan’g:

 int test7b(int y) { int x = x; // expected-note{{variable 'x' is declared here}} if (y) x = 1; // Warn with "may be uninitialized" here (not "is sometimes uninitialized"), // since the self-initialization is intended to suppress a -Wuninitialized // warning. return x; // expected-warning{{variable 'x' may be uninitialized when used here}} } 

Che puoi trovare in clang / test / Sema / uninit-variables.c prova per questo caso esplicitamente.