Perché x = x ++ non è definito?


19

Non è definito perché si modifica xdue volte tra i punti della sequenza. Lo standard dice che non è definito, quindi non è definito.
Questo lo so.

Ma perché?

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

Quindi supponiamo che dovremmo reinventare C oggi. Vorrei suggerire semplici regole 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 i punti di sequenza, l'ordine di valutazione non è specificato.
  2. Gli effetti collaterali si verificano immediatamente.

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

Espressioni di esempio

  1. x=x++- Ben definito, non cambia x.
    Innanzitutto, xviene incrementato (immediatamente quando x++viene valutato), quindi viene archiviato il valore precedente x.

  2. x++ + ++x- Incrementa xdue volte, valuta 2*x+2.
    Sebbene una delle parti possa essere valutata per prima, il risultato è x + (x+2)(prima la parte sinistra) o (x+1) + (x+1)(prima la parte destra).

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

  4. x+=(x=3)- Ben definito, impostato xsu 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 un nuovo valore).

Qual è il vantaggio?

Alcuni commenti hanno sollevato questo buon punto.
Certamente non penso che espressioni come quelle x=x++dovrebbero essere usate in nessun codice normale.
In realtà, sono molto più severo di quello - penso che l'unico buon uso sia x++da x++;solo.

Tuttavia, penso che le regole del linguaggio debbano essere il più semplice possibile. Altrimenti i programmatori non li capiscono. la regola che vieta di cambiare due volte una variabile tra punti di sequenza è certamente una regola che la maggior parte dei programmatori non capisce.

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 ed =è un modo valido per combinare un valore L e un'espressione, quindi come x=x++mai 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: liberati di questa confusione.

=== Riepilogo delle risposte ===

  1. Perché farlo?
    Ho cercato di spiegare nella sezione sopra - Voglio che le regole C siano semplici.

  2. Potenziale di ottimizzazione:
    questo libera un po 'di libertà dal compilatore, ma non ho visto nulla che mi abbia convinto che potrebbe essere significativo.
    La maggior parte delle ottimizzazioni può ancora essere eseguita. Ad esempio, a=3;b=5;può essere riordinato, anche se lo standard specifica l'ordine. Espressioni come quelle a=b[i++]possono ancora essere ottimizzate in modo simile.

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


10
Perché è importante per te? Dovrebbe essere definito, e se sì, perché? Non ha molto senso assegnare xa se stesso, e se vuoi incrementare xpuoi semplicemente dire x++;- non è necessario per il compito. Direi che non dovrebbe essere definito solo perché sarebbe difficile ricordare cosa dovrebbe succedere.
Caleb,

4
Nella mia mente, questa è una buona domanda ("Alcuni uomini vedono le cose come sono e chiedono perché, sogno cose che non sono mai state e chiedo perché no"). È (secondo me) una domanda puramente sulla progettazione del linguaggio, usando la sintassi C come esempio, non una domanda sulla sintassi C. Personalmente, penso che il motivo per cui non abbiamo un comportamento definito per espressioni come x ++ + ++ x o x = x ++ è semplicemente perché esiste la possibilità che vengano interpretate male.
Jamie Taylor,

5
@ugoren: perché è necessario prevedere il risultato. Nessuno nella loro mente corretta scriverebbe un codice del genere (come è stato menzionato più volte) anche se tu scrivessi un codice del genere, questo verrebbe rifiutato alla prima revisione del codice. Quindi non è necessario definire il comportamento e offrire all'ottimizzatore le migliori possibilità di ottimizzarlo. In ogni esempio che proponi, sparerei a qualcuno se lo aggiungesse alla base di codice.
Martin York,

3
Vorrei trovare una domanda più interessante perché non è un errore scrivere questo? Sicuramente un compilatore potrebbe rilevare il suo comportamento indefinito e quindi non può essere quello che l'utente voleva, quindi perché non c'è un errore? Capisco che alcuni casi di bahavour indefinito sono difficili da rilevare, ma questo non lo è; t
JohnB

3
" la regola che vieta di cambiare due volte una variabile tra punti di sequenza è certamente una regola che la maggior parte dei programmatori non capisce. " - Hai qualche prova per questo? La maggior parte delle domande che ho visto sono state poste perché i programmatori non erano a conoscenza della regola. Ci sono prove che la maggior parte di loro non l'ha ancora capito dopo che è stato spiegato?
Sicuro il

Risposte:


24

Forse dovresti prima rispondere alla domanda perché dovrebbe essere definita? Esistono vantaggi in termini di 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 si rompe con la base di codice esistente.


1
Ho aggiunto una sezione "perché" alla mia domanda. Sicuramente non suggerisco di usare queste espressioni, ma sono interessato ad avere regole semplici per dire il significato di un'espressione.
ugoren,

Inoltre, questa modifica non rompe il codice esistente, a meno che non abbia invocato un comportamento indefinito. Correggimi se sbaglio.
ugoren,

3
Bene, una risposta più filosofica: attualmente non è definita. Se nessun programmatore lo utilizza, non è necessario che tu comprenda tali espressioni, perché non dovrebbe esserci alcun codice. Se è necessario che tu li capisca, ovviamente ci deve essere un sacco di codice là fuori che si basa su un comportamento indefinito. ;)
Sicuro il

1
Per definizione, non è rompere alcuna base di codice esistente per definire i comportamenti. Se contenevano UB, erano, per definizione, già rotte.
DeadMG

1
@ugoren: La tua sezione "why" non risponde ancora alla domanda pratica: perché dovresti volere questa strana espressione nel tuo codice? Se non riesci a trovare una risposta convincente, allora l'intera discussione è discutibile.
Mike Baranczak,

20

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

Quando C era nuovo, le macchine che potevano trarne vantaggio per una migliore ottimizzazione erano per lo più modelli teorici. La gente aveva parlato della possibilità di costruire CPU in cui il compilatore avrebbe istruito la CPU su quali istruzioni potevano / dovevano essere eseguite in parallelo con altre istruzioni. Hanno sottolineato il fatto che consentire a ciò di avere un comportamento indefinito significava che su una tale CPU, se mai fosse realmente esistita, si poteva programmare la parte "incremento" dell'istruzione da eseguire in parallelo con il resto del flusso di istruzioni. Sebbene avessero ragione sulla teoria, all'epoca c'era ben poco in termini di hardware che potesse davvero sfruttare questa possibilità.

Non è più solo teorico. Ora c'è hardware in produzione e ampiamente utilizzato (ad es. Itanium, DSP VLIW) che può davvero trarne vantaggio. Hanno davvero fanno permettono al compilatore di generare un flusso di istruzioni che specifica che le istruzioni X, Y e Z possono tutti essere eseguiti in parallelo. Questo non è più un modello teorico: è un vero hardware in uso reale che fa un vero lavoro.

IMO, rendere questo comportamento definito è vicino alla peggiore "soluzione" possibile al problema. Chiaramente non dovresti usare espressioni come questa. Per la stragrande maggioranza del codice, il comportamento ideale sarebbe che il compilatore rifiutasse semplicemente tali espressioni. All'epoca, i compilatori C non eseguivano l'analisi del flusso necessaria per rilevarlo in modo affidabile. Anche ai tempi dello standard C originale, non era ancora del tutto comune.

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

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

Almeno IMO, prendere questo comportamento definito sarebbe (almeno vicino) la decisione peggiore possibile da prendere. Sull'hardware in stile VLIW, la scelta sarebbe quella di generare un codice più lento per gli usi ragionevoli degli operatori di incremento, solo per motivi di codice scadente che li abusa, oppure richiedere sempre un'analisi del flusso approfondita per dimostrare che non si ha a che fare con codice scadente, in modo da poter produrre il codice lento (serializzato) solo quando veramente necessario.

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


IMO, ci sono pochi motivi per credere che nella maggior parte dei casi, le istruzioni più lente sono in realtà molto più lente delle istruzioni veloci e che queste avranno sempre un impatto sulle prestazioni del programma. Classificherei questo con ottimizzazione prematura.
DeadMG

Forse mi manca qualcosa - se nessuno dovrebbe mai scrivere un codice del genere, perché preoccuparsi di ottimizzarlo?
ugoren,

1
@ugoren: scrivere codice come a=b[i++];(per un esempio) va bene, e ottimizzarlo è una buona cosa. Tuttavia, non vedo il punto di danneggiare un codice ragionevole come quello, quindi qualcosa come ++i++ha un significato definito.
Jerry Coffin,

2
@ugoren Il problema è di diagnosi. L'unico scopo di non vietare del tutto espressioni come quelle ++i++è precisamente che in generale è difficile distinguerle da espressioni valide con effetti collaterali (come a=b[i++]). Può sembrare abbastanza semplice per noi, ma se ricordo correttamente il Libro dei draghi , in realtà è un problema NP-difficile. Ecco perché questo comportamento è UB, piuttosto che proibito.
Konrad Rudolph,

1
Non credo che la prestazione sia un argomento valido. Faccio fatica a credere che il caso sia abbastanza comune, considerando la differenza molto ridotta e l'esecuzione molto rapida in entrambi i casi, per rendere evidente un piccolo calo delle prestazioni, per non parlare del fatto che su molti processori e architetture, definirlo è effettivamente gratuito.
DeadMG

9

Eric Lippert, uno dei principali designer del team di compilatori C #, ha pubblicato sul suo blog un articolo su una serie di considerazioni che vanno nella scelta di rendere indefinita una funzionalità a livello di specifiche del linguaggio. Ovviamente C # è un linguaggio diverso, con diversi fattori che vanno nel suo design del linguaggio, ma i punti che fa notare sono comunque rilevanti.

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

Degno di nota è anche, come hai detto, il potenziale prestazionale per l'ottimizzazione del compilatore. Mentre è vero che le prestazioni delle CPU in questi giorni sono molti ordini di grandezza maggiori di quanto lo fossero quando C era giovane, una grande quantità di programmazione C fatta in questi giorni è fatta specificamente a causa del potenziale aumento delle prestazioni e del potenziale (ipotetico futuro ) Le ottimizzazioni delle istruzioni della CPU e le ottimizzazioni dell'elaborazione multicore sarebbero stupide da precludere a causa di un insieme eccessivamente restrittivo di regole per la gestione degli effetti collaterali e dei punti di sequenza.


Dall'articolo a cui ti colleghi, sembra che C # non sia lontano da quello che suggerisco. L'ordinamento degli effetti collaterali è definito "se osservato dal thread che causa gli effetti collaterali". Non ho menzionato il multi-threading, ma in generale C non garantisce molto per un osservatore in un altro thread.
ugoren,

5

Innanzitutto, diamo un'occhiata alla definizione di comportamento indefinito:

3.4.3

1 comportamento
comportamentale indefinito , in seguito all'uso di un costrutto di programma non portabile o errato o di dati errati, per i quali la presente norma internazionale non impone requisiti

2 NOTA Il comportamento possibile non definito va dall'ignorare completamente la situazione con risultati imprevedibili, al comportamento durante la traduzione o l'esecuzione del programma in un modo documentato caratteristico dell'ambiente (con o senza l'emissione di un messaggio diagnostico), per terminare una traduzione o esecuzione (con l'emissione di un messaggio diagnostico).

3 ESEMPIO Un esempio di comportamento indefinito è il comportamento sul overflow del numero intero

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

La radice del problema in discussione è la seguente clausola:

6.5 Espressioni

...
3 Il raggruppamento di operatori e operandi è indicato dalla sintassi. 74) Ad eccezione di quanto speci fi cato in seguito (per la funzione di chiamata (), &&, ||, ?:, e operatori virgola), l'ordine di valutazione di sottoespressioni e l'ordine in cui gli effetti collaterali si svolgono sono entrambi non specificata fi cati .

Enfasi aggiunta.

Data un'espressione simile

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

le sottoespressioni a++, --b, c, e ++dpossono essere valutati in qualsiasi ordine . Inoltre, gli effetti collaterali di a++, --be ++dpossono essere applicati in qualsiasi momento prima del successivo punto di sequenza (IOW, anche se a++valutato prima --b, non è garantito che averrà aggiornato prima della --bvalutazione). Come altri hanno già detto, la logica di questo comportamento è quella di dare all'implementazione la libertà di riordinare le operazioni in modo ottimale.

Per questo motivo, tuttavia, espressioni simili

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

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

Il comportamento non viene definito in modo tale che il compilatore non abbia l'obbligo di "fare la cosa giusta", qualunque essa sia. I casi di cui sopra sono abbastanza facili da rilevare, ma esiste un numero non banale di casi che sarebbe difficile o impossibile da rilevare al momento della compilazione.

Ovviamente, è possibile 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 facciano, in gran parte per evitare i problemi che portano alle definizioni C e C ++.

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 una tale modifica non rompa quel codice. Ciò comporta un po 'di onere per gli autori di compilatori, in quanto tale modifica renderebbe immediatamente non conformi tutti i compilatori esistenti; tutti dovrebbero riscrivere in modo significativo. E anche su CPU moderne e veloci, è ancora possibile realizzare guadagni di prestazioni reali modificando l'ordine di valutazione.


1
Ottima spiegazione del problema. Non sono d'accordo sulla rottura delle applicazioni legacy - il modo in cui viene implementato il comportamento indefinito / non specificato a volte cambia tra la versione del compilatore, senza alcun cambiamento nello standard. Non consiglio di cambiare alcun comportamento definito.
ugoren,

4

Per prima cosa devi capire che non è solo x = x ++ che non è definito. A nessuno importa di x = x ++, dal momento che non importa cosa lo definiresti, non ha senso. Ciò che non è definito è più simile a "a = b ++ in cui aeb sembra essere lo stesso" - vale a dire

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

Esistono diversi modi in cui la funzione può essere implementata, a seconda di ciò che è più efficiente per l'architettura del processore (e per le istruzioni circostanti, nel caso in cui 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 elencato sopra, quello che utilizza più istruzioni e più registri, è quello che si richiederebbe di essere utilizzato in tutti i casi in cui a e b non possono essere dimostrati diversi.


In effetti, mostri un caso in cui il mio suggerimento si traduce in più operazioni della macchina, ma mi sembra insignificante. E il compilatore ha ancora un po 'di libertà - l'unico vero requisito che aggiungo è archiviare bprima a.
ugoren,

3

eredità

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

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


2
Non penso davvero che possiamo reinventare la C oggi. Ciò non significa che non possiamo discutere di questi problemi. Tuttavia, ciò che suggerisco non sta davvero reinventando. La conversione di comportamenti indefiniti in definiti o non specificati può essere eseguita durante l'aggiornamento di uno standard e la lingua sarebbe comunque C.
ugoren

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 fatta valere esplicitamente o implicitamente in molti punti.

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


Non vedo come il mio suggerimento cambi qualcosa in merito p1[i]=p2[j]++. Se il compilatore non può assumere alcun alias, non c'è problema. In caso contrario, deve seguire il libro: incrementare p2[j]prima, conservare p1[i]successivamente. Fatta eccezione per le opportunità di ottimizzazione perse, che non sembrano significative, non vedo alcun problema.
ugoren,

Il secondo paragrafo non era indipendente dal primo, ma un esempio del tipo di luoghi in cui l'assunto può insinuarsi e sarà difficile da rintracciare.
Circa

Il primo paragrafo afferma qualcosa di abbastanza ovvio: i compilatori dovranno essere cambiati per conformarsi a un nuovo standard. Non credo davvero di avere la possibilità di standardizzare questo e far seguire gli autori del compilatore. Penso solo che valga la pena discutere.
ugoren,

Il problema non è che è necessario cambiare i compilatori per qualsiasi cambiamento nella lingua di cui ha bisogno, è che i cambiamenti sono pervasivi e difficili da trovare. L'approccio più pratico sarebbe probabilmente quello di cambiare il formato intermedio su cui funziona l'ottimizzatore, cioè fingere che x = x++;non sia stato scritto ma t = x; x++; x = t;o x=x; x++;o qualunque cosa tu voglia come semantica (ma per quanto riguarda la diagnostica?). Per una nuova lingua, basta eliminare gli effetti collaterali.
Circa

Non so troppo della struttura del compilatore. Se volessi davvero cambiare tutti i compilatori, mi importerebbe di più. Ma forse trattare x++come un punto sequenza, come se fosse una chiamata di funzione, inc_and_return_old(&x)farebbe il trucco.
ugoren,

-1

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


5
Ti interessa elaborare?
ugoren,

Penso che x = ++xora sia ben definito (ma non x = x++)
MM
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.