Cosa ha reso i = i ++ + 1; legale in C ++ 17?


186

Prima di iniziare a urlare comportamenti indefiniti, questo è elencato esplicitamente in N4659 (C ++ 17)

  i = i++ + 1;        // the value of i is incremented

Eppure in N3337 (C ++ 11)

  i = i++ + 1;        // the behavior is undefined

Che cosa è cambiato?

Da quello che posso raccogliere, da [N4659 basic.exec]

Salvo dove indicato, le valutazioni degli operandi dei singoli operatori e delle sottoespressioni delle singole espressioni non sono seguite. [...] I calcoli del valore degli operandi di un operatore sono sequenziati prima del calcolo del valore del risultato dell'operatore. Se un effetto collaterale su una posizione di memoria non è seguito rispetto a un altro effetto collaterale sulla stessa posizione di memoria o a un calcolo del valore che utilizza il valore di qualsiasi oggetto nella stessa posizione di memoria e che non sono potenzialmente concorrenti, il comportamento non è definito.

Dove il valore è definito in [N4659 basic.type]

Per tipi banalmente copiabili, la rappresentazione del valore è un insieme di bit nella rappresentazione dell'oggetto che determina un valore , che è un elemento discreto di un insieme di valori definito dall'implementazione

Da [N3337 basic.exec]

Salvo dove indicato, le valutazioni degli operandi dei singoli operatori e delle sottoespressioni delle singole espressioni non sono seguite. [...] I calcoli del valore degli operandi di un operatore sono sequenziati prima del calcolo del valore del risultato dell'operatore. Se un effetto collaterale su un oggetto scalare non è seguito rispetto a un altro effetto collaterale sullo stesso oggetto scalare o a un calcolo del valore che utilizza il valore dello stesso oggetto scalare, il comportamento non è definito.

Allo stesso modo, il valore è definito in [N3337 basic.type]

Per tipi banalmente copiabili, la rappresentazione del valore è un insieme di bit nella rappresentazione dell'oggetto che determina un valore , che è un elemento discreto di un insieme di valori definito dall'implementazione.

Sono identici ad eccezione della menzione di concorrenza che non ha importanza e dell'uso della posizione della memoria anziché dell'oggetto scalare , dove

I tipi aritmetici, i tipi di enumerazione, i tipi di puntatori, i puntatori ai tipi di membri std::nullptr_te le versioni qualificate per cv di questi tipi sono chiamati collettivamente tipi scalari.

Il che non influisce sull'esempio.

Da [N4659 expr.ass]

L'operatore di assegnazione (=) e gli operatori di assegnazione composti raggruppano tutti da destra a sinistra. Tutti richiedono un valore modificabile come operando sinistro e restituiscono un valore riferito all'operando sinistro. Il risultato in tutti i casi è un campo bit se l'operando di sinistra è un campo bit. In tutti i casi, l'assegnazione viene eseguita in sequenza dopo il calcolo del valore degli operandi destro e sinistro e prima del calcolo del valore dell'espressione di assegnazione. L'operando di destra è sequenziato prima dell'operando di sinistra.

Da [N3337 expr.ass]

L'operatore di assegnazione (=) e gli operatori di assegnazione composti raggruppano tutti da destra a sinistra. Tutti richiedono un valore modificabile come operando sinistro e restituiscono un valore riferito all'operando sinistro. Il risultato in tutti i casi è un campo bit se l'operando di sinistra è un campo bit. In tutti i casi, l'assegnazione viene eseguita in sequenza dopo il calcolo del valore degli operandi destro e sinistro e prima del calcolo del valore dell'espressione di assegnazione.

L'unica differenza è che l'ultima frase è assente in N3337.

L'ultima frase, tuttavia, non dovrebbe avere alcuna importanza in quanto l'operando di sinistra inon è né "un altro effetto collaterale""usa il valore dello stesso oggetto scalare" poiché l' espressione id è un valore.


23
Hai identificato il motivo per cui: In C ++ 17, l'operando di destra è sequenziato prima dell'operando di sinistra. In C ++ 11 non c'era tale sequenziamento. Qual è esattamente la tua domanda?
Robᵩ

4
@ Robᵩ Vedi l'ultima frase.
Passante entro il

7
Qualcuno ha un link alla motivazione di questo cambiamento? Vorrei che un analizzatore statico fosse in grado di dire "non vuoi farlo" di fronte a un codice simile i = i++ + 1;.

7
@NeilButterworth, proviene dall'articolo p0145r3.pdf : " Perfezionamento dell'ordine di valutazione delle espressioni per C ++ idiomatico".
xaizek,

9
@NeilButterworth, la sezione numero 2 dice che questo è contro intuitivo e persino gli esperti non riescono a fare la cosa giusta in tutti i casi. È praticamente tutta la loro motivazione.
xaizek,

Risposte:


144

In C ++ 11 l'atto di "assegnazione", ovvero l'effetto collaterale della modifica dell'LHS, è sequenziato dopo il calcolo del valore dell'operando destro. Si noti che questa è una garanzia relativamente "debole": produce il sequenziamento solo in relazione al calcolo del valore di RHS. Non dice nulla sugli effetti collaterali che potrebbero essere presenti nell'RHS, poiché il verificarsi di effetti collaterali non fa parte del calcolo del valore . I requisiti di C ++ 11 non stabiliscono sequenze relative tra l'atto di assegnazione e gli eventuali effetti collaterali dell'RHS. Questo è ciò che crea il potenziale per UB.

L'unica speranza in questo caso sono eventuali garanzie aggiuntive fornite da operatori specifici utilizzati in RHS. Se RHS utilizzava un prefisso ++, le proprietà di sequenziamento specifiche della forma del prefisso ++avrebbero salvato la giornata in questo esempio. Ma postfix ++è una storia diversa: non offre tali garanzie. In C ++ 11 gli effetti collaterali di =e postfix ++finiscono senza conseguenze l'uno rispetto all'altro in questo esempio. E questo è UB.

In C ++ 17 viene aggiunta una frase aggiuntiva alle specifiche dell'operatore di assegnazione:

L'operando di destra è sequenziato prima dell'operando di sinistra.

In combinazione con quanto sopra offre una garanzia molto forte. Segue tutto ciò che accade nell'RHS (inclusi eventuali effetti collaterali) prima di tutto ciò che accade nell'LHS. Poiché l'assegnazione effettiva è sequenziata dopo LHS (e RHS), tale ulteriore sequenza isola completamente l'atto di assegnazione da qualsiasi effetto collaterale presente in RHS. Questo sequenziamento più forte è ciò che elimina l'UB sopra.

(Aggiornato per tenere conto dei commenti di @John Bollinger.)


3
È davvero corretto includere "l'atto effettivo di assegnazione" negli effetti coperti da "l'operando della mano sinistra" in quell'estratto? Lo standard ha un linguaggio separato per il sequenziamento dell'attribuzione effettiva. Prendo il brano che hai presentato per essere limitato nell'ambito del sequenziamento delle sottoespressioni della mano sinistra e della mano destra, che non sembra essere sufficiente, in combinazione con il resto di quella sezione, per supportare bene- la definizione dell'istruzione del PO.
John Bollinger,

11
Correzione: l'assegnazione effettiva è ancora in sequenza dopo il calcolo del valore dell'operando di sinistra e la valutazione dell'operando di sinistra è in sequenza dopo la valutazione (completa) dell'operando di destra, quindi sì, tale modifica è sufficiente per supportare la ben definita OP Chiesto di. Sto solo cavillando i dettagli, quindi, ma questi sono importanti, poiché potrebbero avere implicazioni diverse per codice diverso.
John Bollinger,

3
@JohnBollinger: Trovo curioso che gli autori dello Standard apporterebbero un cambiamento che compromette l'efficienza della generazione di codice anche semplice e che storicamente non è stato necessario, e tuttavia si oppongono nel definire altri comportamenti la cui assenza è un problema molto più grande, e che raramente costituirebbe un ostacolo significativo all'efficienza.
supercat

1
@Kaz: per le assegnazioni composte, eseguire la valutazione del valore sul lato sinistro dopo il lato destro consente x -= y;di elaborare qualcosa di simile mov eax,[y] / sub [x],eaxanziché mov eax,[x] / neg eax / add eax,[y] / mov [x],eax. Non vedo nulla di idiodico al riguardo. Se si dovesse specificare un ordinamento, l'ordinamento più efficiente sarebbe probabilmente quello di eseguire tutti i calcoli necessari per identificare prima l'oggetto lato sinistro, quindi valutare l'operando destro, quindi il valore dell'oggetto sinistro, ma ciò richiederebbe un termine per l'atto di risolvere l'identità dell'oggetto sinistro.
supercat

1
@Kaz: Se xe yfosse volatile, ciò avrebbe effetti collaterali. Inoltre, le stesse considerazioni si applicano a x += f();, dove f()modifica x.
supercat

33

Hai identificato la nuova frase

L'operando di destra è sequenziato prima dell'operando di sinistra.

e hai correttamente identificato che la valutazione dell'operando di sinistra come un valore è irrilevante. Tuttavia, in precedenza è specificato che è una relazione transitiva. L'operando di destra completo (incluso il post-incremento) è quindi anche sequenziato prima dell'assegnazione. In C ++ 11, solo il calcolo del valore dell'operando di destra era in sequenza prima dell'assegnazione.


7

Negli standard C ++ precedenti e in C11, la definizione del testo dell'operatore di assegnazione termina con il testo:

Le valutazioni degli operandi non sono seguite.

Ciò significa che gli effetti collaterali negli operandi sono senza conseguenze e quindi sicuramente un comportamento indefinito se usano la stessa variabile.

Questo testo è stato semplicemente rimosso in C ++ 11, lasciandolo in qualche modo ambiguo. È UB o no? Questo è stato chiarito in C ++ 17 dove hanno aggiunto:

L'operando di destra è sequenziato prima dell'operando di sinistra.


Come nota a margine, in standard ancora più vecchi, tutto ciò è stato chiarito, ad esempio da C99:

L'ordine di valutazione degli operandi non è specificato. Se si tenta di modificare il risultato di un operatore di assegnazione o di accedervi dopo il successivo punto di sequenza, il comportamento non è definito.

Fondamentalmente, in C11 / C ++ 11, hanno incasinato quando hanno rimosso questo testo.


1

Queste sono ulteriori informazioni alle altre risposte e le sto pubblicando poiché viene spesso richiesto anche il codice seguente .

La spiegazione nelle altre risposte è corretta e si applica anche al seguente codice che ora è ben definito (e non modifica il valore memorizzato di i):

i = i++;

Si + 1tratta di un'aringa rossa e non è molto chiaro il motivo per cui lo Standard lo abbia usato nei loro esempi, anche se ricordo persone che discutevano su mailing list prima di C ++ 11 che forse hanno + 1fatto la differenza a causa della forzatura della conversione anticipata del valore a destra- lato della mano. Certamente nulla di tutto ciò si applica in C ++ 17 (e probabilmente mai applicato in nessuna versione di C ++).

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.