Perché x = x ++ non definito?

19

Non è definito perché modifica x due volte tra i punti di sequenza. Lo standard dice che è indefinito, quindi non è definito.
Questo lo so.

Ma perché?

La mia comprensione è che vietare ciò consente ai compilatori di ottimizzare meglio. Questo avrebbe potuto avere senso quando C fu inventato, ma ora sembra un argomento debole.
Se dovessimo reinventare C oggi, lo faremmo in questo modo, o si potrebbe fare meglio?
O forse c'è un problema più profondo, che rende difficile definire regole coerenti per tali espressioni, quindi è meglio vietarle?

Quindi supponiamo di reinventare C oggi. Vorrei suggerire regole semplici per espressioni come x=x++ , che mi sembrano funzionare meglio delle regole esistenti.
Mi piacerebbe avere la tua opinione sulle regole suggerite rispetto a quelle esistenti o altri suggerimenti.

Regole suggerite:

  1. Tra punti di sequenza, l'ordine di valutazione non è specificato.
  2. Gli effetti collaterali avvengono immediatamente.

Non sono coinvolti comportamenti indefiniti. Le espressioni valutano questo valore o quello, ma sicuramente non formatteranno il tuo hard disk (stranamente, non ho mai visto un'implementazione dove x=x++ formatta il disco rigido).

Espressioni di esempio

  1. x=x++ - Ben definito, non modifica x .
    Innanzitutto, x viene incrementato (immediatamente quando viene valutato x++ ), quindi il suo vecchio valore è memorizzato in x .

  2. x++ + ++x - Incrementi x due volte, restituisce 2*x+2 .
    Sebbene entrambi i lati possano essere valutati per primi, il risultato è x + (x+2) (lato sinistro prima) o (x+1) + (x+1) (lato destro prima).

  3. x = x + (x=3) - Non specificato, x impostato su x+3 o 6 .
    Se il lato destro viene valutato per primo, è x+3 . È anche possibile che x=3 sia valutato per primo, quindi è 3+3 . In entrambi i casi, l'assegnazione di x=3 avviene immediatamente quando viene valutato x=3 , quindi il valore memorizzato viene sovrascritto dall'altra assegnazione.

  4. x+=(x=3) - Ben definito, imposta x su 6.
    Si potrebbe sostenere che questa è solo una scorciatoia per l'espressione sopra.
    Ma direi che += deve essere eseguito dopo x=3 , e non in due parti (leggi x , valuta x=3 , aggiungi e memorizza nuovo valore).

Qual è il vantaggio?

Alcuni commenti hanno sollevato questo buon punto.
Certamente non penso che espressioni come x=x++ debbano essere usate in qualsiasi codice normale.
In realtà, sono molto più severo di così: penso che l'unico utilizzo valido per x++ sia in x++; .

Tuttavia, penso che le regole linguistiche debbano essere il più semplici possibile. Altrimenti i programmatori semplicemente non li capiscono. la regola che vieta di cambiare una variabile due volte tra i punti di sequenza è certamente una regola che molti programmatori non capiscono.

Una regola molto semplice è questa:
Se A è valido e B è valido e sono combinati in modo valido, il risultato è valido.
x è un valore L valido, x++ è un'espressione valida e = è un modo valido per combinare un valore L e un'espressione, quindi come mai x=x++ non è legale?
Lo standard C fa un'eccezione qui e questa eccezione complica le regole. Puoi cercare stackoverflow.com e vedere quanto questa eccezione confonde le persone.
Quindi dico: sbarazzati di questa confusione.

=== Riepilogo delle risposte ===

  1. Perché farlo?
    Ho cercato di spiegare nella sezione precedente: voglio che le regole C siano semplici.

  2. Potenziale di ottimizzazione:
    Ciò richiede una certa libertà dal compilatore, ma non ho visto nulla che mi abbia convinto che potrebbe essere significativo.
    La maggior parte delle ottimizzazioni può ancora essere fatta. Ad esempio, a=3;b=5; può essere riordinato, anche se lo standard specifica l'ordine. Espressioni come a=b[i++] possono ancora essere ottimizzate allo stesso modo.

  3. Non è possibile modificare lo standard esistente.
    Lo ammetto, non posso. Non ho mai pensato di poter effettivamente andare avanti e cambiare standard e compilatori. Volevo solo pensare se le cose avrebbero potuto essere fatte in modo diverso.

posta ugoren 19.06.2012 - 09:24
fonte

8 risposte

24

Forse dovresti prima rispondere alla domanda sul perché dovrebbe essere definito? C'è qualche vantaggio nello stile di programmazione, leggibilità, manutenibilità o prestazioni consentendo tali espressioni con effetti collaterali aggiuntivi? È

y = x++ + ++x;

più leggibile di

y = 2*x + 2;
x += 2;

Dato che un tale cambiamento è estremamente fondamentale e irrompe nella base di codice esistente.

    
risposta data 19.06.2012 - 09:49
fonte
20

L'argomento secondo cui rendere questo comportamento indefinito consente una migliore ottimizzazione è non debole oggi. In effetti, oggi è più strong di quanto non fosse quando C era nuovo.

Quando C era nuovo, le macchine che potevano trarne vantaggio per una migliore ottimizzazione erano per lo più modelli teorici. Le persone avevano parlato della possibilità di costruire CPU dove il compilatore avrebbe istruito la CPU su quali istruzioni potevano / dovrebbero essere eseguite in parallelo con altre istruzioni. Hanno sottolineato il fatto che consentire a questo di avere un comportamento indefinito significava che su una CPU del genere, se mai esisteva realmente, era possibile pianificare la parte di "incremento" dell'istruzione da eseguire in parallelo con il resto del flusso di istruzioni. Mentre avevano ragione riguardo alla teoria, al momento c'era poco hardware che poteva davvero approfittare di questa possibilità.

Questo non è più solo teorico. Ora l'hardware è in produzione e ampiamente utilizzato (ad es. Itanium, VSP DLI VL) che può davvero trarre vantaggio da questo. In realtà fanno permettono al compilatore di generare un flusso di istruzioni che specifica che le istruzioni X, Y e Z possono essere eseguite tutte in parallelo. Questo non è più un modello teorico - è un vero hardware in uso reale che fa un vero lavoro.

IMO, rendendo questo comportamento definito vicino alla peggiore possibile "soluzione" al problema. Ovviamente non dovresti usare espressioni come questa. Per la stragrande maggioranza del codice, il comportamento ideale sarebbe che il compilatore rifiutasse semplicemente tali espressioni interamente. Al momento, i compilatori C non eseguivano l'analisi del flusso necessaria per rilevarlo in modo affidabile. Anche al momento dello standard C originale, non era affatto comune.

Non sono sicuro che sarebbe accettabile per la comunità oggi - mentre molti compilatori possono fare questo tipo di analisi del flusso, in genere lo fanno solo quando si richiede l'ottimizzazione. Dubito che la maggior parte dei programmatori gradirebbe l'idea di rallentare le build di "debug" solo per il gusto di poter rifiutare il codice che (essendo sane) non scriverebbe mai in primo luogo.

Ciò che C ha fatto è una seconda scelta semi-ragionevole: dire alla gente di non farlo, permettendo (ma non richiedendo) al compilatore di rifiutare il codice. Questo evita (ancora più lontano) il rallentamento della compilazione per le persone che non lo userebbero mai, ma consente comunque a qualcuno di scrivere un compilatore che rifiuta tale codice se lo desidera (e / o ha dei flag che lo rifiuteranno che le persone possono scegliere di usare o non come loro credono).

Almeno IMO, rendendo questo comportamento definito sarebbe (almeno vicino) la peggiore possibile decisione da prendere. Su hardware in stile VLIW, le tue scelte consistono nel generare codice più lento per gli usi ragionevoli degli operatori di incremento, solo per motivi di codice crappy che li abusa, oppure richiedono sempre un'analisi approfondita del flusso per dimostrare che non hai a che fare con codice scadente, quindi puoi produrre il codice lento (serializzato) solo se veramente necessario.

In conclusione: se vuoi risolvere questo problema, dovresti pensare nella direzione opposta. Invece di definire cosa fa tale codice, dovresti definire la lingua in modo tale che semplicemente tali espressioni non siano affatto consentite (e convivere con il fatto che la maggior parte dei programmatori probabilmente opterà per una compilazione più rapida rispetto all'applicazione di tale requisito).

    
risposta data 19.06.2012 - 14:09
fonte
9

Eric Lippert, uno dei principali progettisti del team di compilatori C #, ha pubblicato sul suo blog un articolo su una serie di considerazioni che vanno nella scelta di rendere una funzione non definita al livello delle specifiche della lingua. Ovviamente C # è un linguaggio diverso, con diversi fattori che entrano nella sua progettazione linguistica, ma i punti che egli fa sono comunque rilevanti.

In particolare, sottolinea il problema di avere compilatori esistenti per una lingua che hanno implementazioni esistenti e hanno anche rappresentanti in una commissione. Non sono sicuro se questo è il caso qui, ma tende ad essere rilevante per la maggior parte delle discussioni sulle specifiche relative a C e C ++.

Inoltre, come hai detto, il potenziale di rendimento per l'ottimizzazione del compilatore è anche di rilievo. Mentre è vero che le prestazioni delle CPU in questi giorni sono di molti ordini di grandezza maggiori rispetto a quando C era giovane, una grande quantità di programmazione C fatta in questi giorni è fatta specificamente a causa del potenziale guadagno di prestazioni e del potenziale (futuro ipotetico ) Ottimizzazioni dell'istruzione della CPU e ottimizzazioni dell'elaborazione multicore sarebbero stupide da escludere a causa di un insieme di regole eccessivamente restrittive per la gestione di effetti collaterali e punti di sequenza.

    
risposta data 19.06.2012 - 10:07
fonte
5

Per prima cosa, diamo un'occhiata alla definizione di indefinito comportamento:

3.4.3

1 undefined behavior
behavior, upon use of a nonportable or erroneous program construct or of erroneous data, for which this International Standard imposes no requirements

2 NOTE Possible undefined behavior ranges from ignoring the situation completely with unpredictable results, to behaving during translation or program execution in a documented manner characteristic of the environment (with or without the issuance of a diagnostic message), to terminating a translation or execution (with the issuance of a diagnostic message).

3 EXAMPLE An example of undefined behavior is the behavior on integer overflow

Quindi, in altre parole, "comportamento non definito" significa semplicemente che il compilatore è libero di gestire la situazione in qualsiasi modo voglia, e qualsiasi azione del genere è considerata "corretta".

La radice del problema in discussione è la seguente clausola:

6.5 Expressions

...
3 The grouping of operators and operands is indicated by the syntax. 74) Except as specified later (for the function-call (), &&, ||, ?:, and comma operators), the order of evaluation of subexpressions and the order in which side effects take place are both unspecified.

Enfasi aggiunta.

Dato un'espressione come

x = a++ * --b / (c + ++d);

le sottoespressioni a++ , --b , c e ++d possono essere valutate in qualsiasi ordine . Inoltre, gli effetti collaterali di a++ , --b e ++d possono essere applicati in qualsiasi punto prima del successivo punto di sequenza (IOW, anche se a++ viene valutato prima di --b , non è garantito che a sarà aggiornato prima che venga valutata --b ). Come altri hanno già detto, la logica di questo comportamento è dare all'implementazione la libertà di riordinare le operazioni in modo ottimale.

Per questo motivo, tuttavia, espressioni come

x = x++
y = i++ * i++
a[i] = i++
*p++ = -*p    // this one bit me just yesterday

ecc., produrrà risultati diversi per diverse implementazioni (o per la stessa implementazione con diverse impostazioni di ottimizzazione, o in base al codice circostante, ecc.).

Il comportamento è lasciato non definito in modo che il compilatore non abbia l'obbligo di "fare la cosa giusta", qualunque esso sia. I casi di cui sopra sono abbastanza facili da catturare, ma esiste un numero non banale di casi che sarebbe difficile da catturare in fase di compilazione.

Ovviamente, puoi progettare un linguaggio tale che l'ordine di valutazione e l'ordine in cui vengono applicati gli effetti collaterali siano rigorosamente definiti, e sia Java che C # lo fanno, in gran parte per evitare i problemi che Le definizioni C e C ++ portano a.

Quindi, perché questa modifica non è stata apportata a C dopo 3 revisioni standard? Prima di tutto, ci sono 40 anni di codice C legacy là fuori, e non è garantito che un tale cambiamento non infrangerà quel codice. Pone un po 'di peso sugli scrittori di compilatori, in quanto tale modifica renderebbe immediatamente tutti i compilatori esistenti non conformi; tutti dovrebbero fare riscritture significative. E anche su CPU moderne e veloci, è ancora possibile realizzare guadagni di prestazioni reali modificando l'ordine di valutazione.

    
risposta data 19.06.2012 - 18:03
fonte
4

Prima devi capire che non è solo x = x ++ che non è definito. A nessuno importa di x = x ++, poiché non importa come lo definiresti, non ha senso. Quello che non è definito è più come "a = b ++ dove a e b capita di essere lo stesso" - cioè

void f(int *a, int *b) {
    *a = (*b)++;
}
int i;
f(&i, &i);

Esistono diversi modi in cui la funzione potrebbe essere implementata, a seconda di ciò che è più efficiente per l'architettura del processore (e per le istruzioni circostanti, nel caso questa sia una funzione più complessa dell'esempio). Ad esempio, due ovvi:

load r1 = *b
copy r2 = r1
increment r1
store *b = r1
store *a = r2

o

load r1 = *b
store *a = r1
increment r1
store *b = r1

Si noti che il primo sopra elencato, quello che utilizza più istruzioni e più registri, è quello che si vorrebbe utilizzare in tutti i casi in cui a e b non possono essere dimostrati come diversi.

    
risposta data 19.06.2012 - 19:42
fonte
3

Legacy

L'ipotesi che C possa essere reinventata oggi non può reggere. Ci sono così tante righe di codici C che sono state prodotte e usate quotidianamente, che cambiare le regole del gioco nel bel mezzo del gioco è semplicemente sbagliato.

Ovviamente puoi inventare una nuova lingua, ad esempio C + = , con le tue regole. Ma quello non sarà C.

    
risposta data 19.06.2012 - 09:43
fonte
2

Dichiarare che qualcosa è definito non cambierà i compilatori esistenti per rispettare la tua definizione. Ciò è particolarmente vero nel caso di un'ipotesi che potrebbe essere stata invocata in modo esplicito o implicito in molti luoghi.

Il problema principale per l'ipotesi non è con x = x++; (i compilatori possono facilmente controllarlo e dovrebbero avvisare), è con *p1 = (*p2)++ e equivalente ( p1[i] = p2[j]++; quando p1 e p2 sono parametri di una funzione) dove il compilatore non può sapere facilmente se p1 == p2 (in C99 restrict è stato aggiunto per distribuire la possibilità di assumere p1! = p2 tra punti di sequenza, quindi si è ritenuto che le possibilità di ottimizzazione fossero importanti).

    
risposta data 19.06.2012 - 13:54
fonte
-1

In alcuni casi, questo tipo di codice è stato definito nel nuovo standard C ++ 11.

    
risposta data 19.06.2012 - 14:31
fonte

Leggi altre domande sui tag