Perché questi costrutti utilizzano comportamenti indefiniti pre e post-incremento?


815
#include <stdio.h>

int main(void)
{
   int i = 0;
   i = i++ + ++i;
   printf("%d\n", i); // 3

   i = 1;
   i = (i++);
   printf("%d\n", i); // 2 Should be 1, no ?

   volatile int u = 0;
   u = u++ + ++u;
   printf("%d\n", u); // 1

   u = 1;
   u = (u++);
   printf("%d\n", u); // 2 Should also be one, no ?

   register int v = 0;
   v = v++ + ++v;
   printf("%d\n", v); // 3 (Should be the same as u ?)

   int w = 0;
   printf("%d %d\n", ++w, w); // shouldn't this print 1 1

   int x[2] = { 5, 8 }, y = 0;
   x[y] = y ++;
   printf("%d %d\n", x[0], x[1]); // shouldn't this print 0 8? or 5 0?
}

12
@Jarett, no, avevo solo bisogno di alcuni suggerimenti per "punti di sequenza". Mentre lavoro ho trovato un pezzo di codice con i = i ++, ho pensato "Questo non sta modificando il valore di i". Ho provato e mi chiedevo perché. Da allora, ho rimosso questa affermazione e sostituita da i ++;
PiX

198
Penso che sia interessante che tutti presumano SEMPRE che domande come queste vengano poste perché chi chiede vuole USARE il costrutto in questione. La mia prima ipotesi è stata che PiX sappia che questi sono cattivi, ma è curioso di sapere come si comportano in questo modo sul compilatore di whataver che stava usando ... E sì, cosa ha detto Wind ... non è definito, potrebbe fare qualsiasi cosa. .. tra cui JCF (Jump and Catch Fire)
Brian Postow,

32
Sono curioso: perché i compilatori non sembrano avvertire costrutti come "u = u ++ + ++ u;" se il risultato non è definito?
Impara OpenGL ES il

5
(i++)valuta ancora 1, indipendentemente dalle parentesi
Drew McGowen il

2
Qualunque cosa i = (i++);si intendesse fare, c'è sicuramente un modo più chiaro per scriverlo. Ciò sarebbe vero anche se fosse ben definito. Anche in Java, che definisce il comportamento di i = (i++);, è ancora un codice errato. Basta scriverei++;
Keith Thompson il

Risposte:


566

C ha il concetto di comportamento indefinito, ovvero alcuni costrutti di linguaggio sono sintatticamente validi ma non è possibile prevedere il comportamento quando viene eseguito il codice.

Per quanto ne so, lo standard non dice esplicitamente perché esiste il concetto di comportamento indefinito. Nella mia mente, è semplicemente perché i progettisti del linguaggio volevano che ci fosse un certo margine di manovra nella semantica, invece di richiedere cioè che tutte le implementazioni gestissero l'overflow di numeri interi nello stesso identico modo, il che probabilmente avrebbe imposto gravi costi di prestazione, hanno semplicemente lasciato il comportamento indefinito in modo che se si scrive codice che provoca overflow di numeri interi, può succedere di tutto.

Quindi, con questo in mente, perché questi "problemi"? La lingua dice chiaramente che certe cose portano a comportamenti indefiniti . Non c'è problema, non c'è "dovrebbe" coinvolto. Se il comportamento indefinito cambia quando viene dichiarata una delle variabili coinvolte volatile, ciò non prova o cambia nulla. Non è definito ; non puoi ragionare sul comportamento.

Il tuo esempio dall'aspetto più interessante, quello con

u = (u++);

è un esempio da manuale di comportamento indefinito (vedere la voce di Wikipedia sui punti di sequenza ).


8
@PiX: le cose non sono definite per una serie di possibili motivi. Questi includono: non esiste un "risultato giusto" chiaro, architetture di macchine diverse favorirebbero fortemente risultati diversi, la pratica esistente non è coerente o esula dallo scopo dello standard (ad esempio quali nomi di file sono validi).
Richard,

Solo per confondere tutti, alcuni di questi esempi sono ora ben definiti in C11, ad es i = ++i + 1;.
MM

2
Leggendo lo standard e la logica pubblicata, è chiaro perché esiste il concetto di UB. Lo standard non è mai stato concepito per descrivere in modo completo tutto ciò che un'implementazione C deve fare per essere adatta a qualsiasi scopo particolare (vedere la discussione sulla regola del "Programma unico"), ma si basa invece sul giudizio degli attuatori e sul desiderio di produrre utili implementazioni di qualità. Un'implementazione di qualità adatta per la programmazione di sistemi di basso livello dovrà definire il comportamento delle azioni che non sarebbero necessarie nella compressione dei numeri di fascia alta. Invece di provare a complicare lo Standard ...
Supercat,

3
... entrando nel dettaglio estremo su quali casi angolari sono o non sono definiti, gli autori dello Standard hanno riconosciuto che gli implementatori dovrebbero essere meglio stimolati per giudicare quali tipi di comportamenti saranno necessari dai tipi di programmi che dovrebbero supportare . I compilatori iper-modernisti fingono che compiere determinate azioni UB intendesse implicare che nessun programma di qualità dovrebbe averne bisogno, ma lo Standard e la logica non sono coerenti con un tale intento.
supercat

1
@jrh: ho scritto quella risposta prima di rendermi conto di quanto la filosofia iper-modernista fosse sfuggita di mano. Ciò che mi infastidisce è la progressione da "Non abbiamo bisogno di riconoscere ufficialmente questo comportamento perché le piattaforme in cui è necessario possono supportarlo comunque" a "Possiamo rimuovere questo comportamento senza fornire una sostituzione utilizzabile perché non è mai stato riconosciuto e quindi nessun codice averne bisogno era rotto ". Molti comportamenti avrebbero dovuto essere deprecati molto tempo fa a favore di sostituzioni che erano in ogni modo migliori , ma che avrebbero richiesto il riconoscimento della loro legittimità.
supercat

78

Basta compilare e disassemblare la riga di codice, se si è così inclini a sapere esattamente come si ottiene ciò che si ottiene.

Questo è ciò che ottengo sulla mia macchina, insieme a ciò che penso stia succedendo:

$ cat evil.c
void evil(){
  int i = 0;
  i+= i++ + ++i;
}
$ gcc evil.c -c -o evil.bin
$ gdb evil.bin
(gdb) disassemble evil
Dump of assembler code for function evil:
   0x00000000 <+0>:   push   %ebp
   0x00000001 <+1>:   mov    %esp,%ebp
   0x00000003 <+3>:   sub    $0x10,%esp
   0x00000006 <+6>:   movl   $0x0,-0x4(%ebp)  // i = 0   i = 0
   0x0000000d <+13>:  addl   $0x1,-0x4(%ebp)  // i++     i = 1
   0x00000011 <+17>:  mov    -0x4(%ebp),%eax  // j = i   i = 1  j = 1
   0x00000014 <+20>:  add    %eax,%eax        // j += j  i = 1  j = 2
   0x00000016 <+22>:  add    %eax,-0x4(%ebp)  // i += j  i = 3
   0x00000019 <+25>:  addl   $0x1,-0x4(%ebp)  // i++     i = 4
   0x0000001d <+29>:  leave  
   0x0000001e <+30>:  ret
End of assembler dump.

(Suppongo che l'istruzione 0x00000014 fosse una sorta di ottimizzazione del compilatore?)


come ottengo il codice macchina? Uso Dev C ++ e ho giocato con l'opzione 'Generazione del codice' nelle impostazioni del compilatore, ma non ho alcun output di file aggiuntivo o alcun output di console
bad_keypoints

5
@ronnieaka gcc evil.c -c -o evil.bine gdb evil.bindisassemble evil, o qualunque siano gli equivalenti di Windows di quelli :)
badp

21
Questa risposta non affronta davvero la domanda di Why are these constructs undefined behavior?.
Shafik Yaghmour,

9
Per inciso, sarà più semplice compilare in assembly (con gcc -S evil.c), che è tutto ciò che serve qui. Montare e poi smontare è solo un modo rotatorio di farlo.
Kat,

50
Per la cronaca, se per qualsiasi motivo ti stai chiedendo cosa fa un determinato costrutto - e soprattutto se c'è il sospetto che possa essere un comportamento indefinito, il consiglio secolare di "provalo con il tuo compilatore e vedi" è potenzialmente abbastanza pericoloso. Imparerai, nella migliore delle ipotesi, cosa fa in questa versione del tuo compilatore, in queste circostanze, oggi . Si Non imparare molto se nulla di ciò che è garantito per fare. In generale, "provalo con il tuo compilatore" porta a programmi non portabili che funzionano solo con il tuo compilatore.
Steve Summit,

64

Penso che le parti rilevanti dello standard C99 siano 6.5 Expressions, §2

Tra il punto di sequenza precedente e quello successivo, un oggetto deve avere il suo valore memorizzato modificato al massimo una volta dalla valutazione di un'espressione. Inoltre, il valore precedente deve essere letto solo per determinare il valore da memorizzare.

e 6.5.16 Operatori di assegnazione, §4:

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.


2
Quanto sopra implicherebbe che 'i = i = 5; "sarebbe un comportamento indefinito?
supercat

1
@supercat per quanto ne so i=i=5è anche un comportamento indefinito
dhein

2
@Zaibis: la logica che mi piace usare per la maggior parte dei luoghi regola che in teoria una piattaforma multiprocessore potrebbe implementare qualcosa come A=B=5;"Blocco-scrittura A; Blocco-scrittura B; Memorizza 5 su A; Memorizza 5 su B; Sblocca B ; Unock A; "e un'istruzione del tipo C=A+B;" Blocco lettura A; Blocco lettura B; Calcolo A + B; Sblocco A e B; Blocco scrittura C; Risultato della memorizzazione; Sblocco C; ". Ciò garantirebbe che se un thread ha fatto A=B=5;mentre un altro ha fatto C=A+B;quest'ultimo thread vedrebbe entrambe le scritture come aver avuto luogo o nessuno dei due. Potenzialmente una garanzia utile. Se un thread ha fatto I=I=5;, tuttavia, ...
supercat

1
... e il compilatore non ha notato che entrambe le scritture si trovavano nella stessa posizione (se uno o entrambi i valori includevano puntatori, che potrebbe essere difficile da determinare), il codice generato potrebbe bloccarsi. Non credo che le implementazioni del mondo reale implementino tale blocco come parte del loro comportamento normale, ma sarebbe consentito in base allo standard e se l'hardware potesse implementare tali comportamenti a basso costo potrebbe essere utile. Sull'hardware di oggi tale comportamento sarebbe troppo costoso da implementare come predefinito, ma ciò non significa che sarebbe sempre così.
supercat,

1
@supercat ma la regola di accesso al punto sequenza del solo c99 non sarebbe sufficiente per dichiararlo come comportamento indefinito? Quindi non importa cosa tecnicamente l'hardware potrebbe implementare?
Dhein,

55

La maggior parte delle risposte qui citate dallo standard C sottolinea che il comportamento di questi costrutti non è definito. Per capire perché il comportamento di questi costrutti non è definito , capiamo prima questi termini alla luce dello standard C11:

Sequenziato: (5.1.2.3)

Alla luce di due valutazioni Ae B, se Aprecedentemente sequenziate B, l'esecuzione di Adeve precedere l'esecuzione di B.

non in sequenza:

Se Anon è in sequenza prima o dopo B, allora Ae non Bsono seguiti.

Le valutazioni possono essere una delle due cose:

  • calcoli di valore , che elaborano il risultato di un'espressione; e
  • effetti collaterali , che sono modifiche degli oggetti.

Punto sequenza:

La presenza di un punto di sequenza tra la valutazione delle espressioni Ae Bimplica che ogni calcolo di valore ed effetto collaterale associato Aè sequenziato prima di ogni calcolo di valore ed effetto collaterale associato B.

Ora veniamo alla domanda, per le espressioni simili

int i = 1;
i = i++;

lo standard dice che:

6.5 Espressioni:

Se un effetto collaterale su un oggetto scalare è non in sequenza rispetto ad entrambi un diverso effetto collaterale sullo stesso oggetto scalare o un calcolo valore utilizzando il valore dello stesso oggetto scalare, è definito il comportamento . [...]

Pertanto, l'espressione sopra invoca UB perché due effetti collaterali sullo stesso oggetto non isono conseguiti l'uno rispetto all'altro. Ciò significa che non è in sequenza se l'effetto collaterale per assegnazione iverrà effettuato prima o dopo l'effetto collaterale di ++.
A seconda che l'assegnazione avvenga prima o dopo l'incremento, verranno prodotti risultati diversi e questo è il caso di un comportamento indefinito .

Consente di rinominare ibe a sinistra del compito ile a destra del compito (nell'espressione i++) essere ir, quindi l'espressione è come

il = ir++     // Note that suffix l and r are used for the sake of clarity.
              // Both il and ir represents the same object.  

Un punto importante per quanto riguarda l' ++operatore Postfix è che:

solo perché il ++dopo la variabile non significa che l'incremento avviene in ritardo . L'incremento può avvenire non appena il compilatore piace purché il compilatore assicuri che venga utilizzato il valore originale .

Significa che l'espressione il = ir++potrebbe essere valutata come

temp = ir;      // i = 1
ir = ir + 1;    // i = 2   side effect by ++ before assignment
il = temp;      // i = 1   result is 1  

o

temp = ir;      // i = 1
il = temp;      // i = 1   side effect by assignment before ++
ir = ir + 1;    // i = 2   result is 2  

risultando in due risultati diversi 1e 2che dipende dalla sequenza di effetti collaterali per incarico ++e quindi invoca UB.


52

Il comportamento non può davvero essere spiegato perché invoca sia un comportamento non specificato e comportamento non definito , quindi non possiamo fare previsioni generali su questo codice, anche se leggete di Olve Maudal lavoro, come profonda C e non specificato e non definita a volte si può fare un buon indovina in casi molto specifici con un compilatore e un ambiente specifici, ma per favore non farlo da nessuna parte vicino alla produzione.

Passando quindi a comportamenti non specificati , nella bozza della sezione standard c99 il6.5 paragrafo 3 dice ( enfasi sulla mia ):

Il raggruppamento di operatori e operandi è indicato dalla sintassi.74) Ad eccezione di quanto specificato più avanti (per gli operatori call-call (), &&, ||,?: E virgola), l'ordine di valutazione delle sottoespressioni e l'ordine in quali effetti collaterali si verificano sono entrambi non specificati.

Quindi quando abbiamo una linea come questa:

i = i++ + ++i;

non sappiamo se i++o ++isaranno valutati per primi. Questo principalmente per offrire al compilatore migliori opzioni di ottimizzazione .

Abbiamo anche un comportamento indefinito anche qui in quanto il programma sta modificando le variabili ( i, u, ecc ..) più di una volta tra i punti di sequenza . Dalla bozza della sezione standard 6.5paragrafo 2 ( sottolineatura mia ):

Tra il punto di sequenza precedente e quello successivo un oggetto deve avere il suo valore memorizzato modificato al massimo una volta dalla valutazione di un'espressione. Inoltre, il valore precedente deve essere letto solo per determinare il valore da memorizzare .

cita i seguenti esempi di codice come non definiti:

i = ++i + 1;
a[i++] = i; 

In tutti questi esempi il codice sta tentando di modificare un oggetto più di una volta nello stesso punto sequenza, che terminerà con ;in ciascuno di questi casi:

i = i++ + ++i;
^   ^       ^

i = (i++);
^    ^

u = u++ + ++u;
^   ^       ^

u = (u++);
^    ^

v = v++ + ++v;
^   ^       ^

Il comportamento non specificato è definito nella bozza della norma c99 nella sezione 3.4.4come:

utilizzo di un valore non specificato o altro comportamento in cui la presente norma internazionale offre due o più possibilità e non impone ulteriori requisiti su quale sia scelta in ogni caso

e il comportamento indefinito è definito nella sezione 3.4.3come:

comportamento, in caso di utilizzo di un costrutto di programma non portabile o errato o di dati errati, per i quali la presente norma internazionale non impone requisiti

e osserva che:

I possibili comportamenti indefiniti vanno 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), alla conclusione di una traduzione o esecuzione (con l'emissione di un messaggio diagnostico).


33

Un altro modo di rispondere a questo, piuttosto che impantanarsi in dettagli arcani di punti sequenza e comportamento indefinito, è semplicemente quello di chiedere, cosa dovrebbero significare? Cosa stava cercando di fare il programmatore?

Il primo frammento di cui si chiede i = i++ + ++i, è abbastanza chiaramente folle nel mio libro. Nessuno lo scriverebbe mai in un vero programma, non è ovvio ciò che fa, non esiste un algoritmo concepibile che qualcuno avrebbe potuto provare a codificare che avrebbe portato a questa particolare sequenza di operazioni inventata. E dal momento che non è ovvio per te e per me cosa dovrebbe fare, va bene anche nel mio libro se il compilatore non riesce a capire cosa dovrebbe fare.

Il secondo frammento, i = i++è un po 'più facile da capire. Qualcuno sta chiaramente cercando di incrementare i e di assegnare il risultato a i. Ma ci sono un paio di modi per farlo in C. Il modo più semplice per aggiungere 1 a i e assegnare il risultato a i è lo stesso in quasi tutti i linguaggi di programmazione:

i = i + 1

C, ovviamente, ha una comoda scorciatoia:

i++

Ciò significa "aggiungi 1 a i e assegna il risultato a i". Quindi, se costruiamo un miscuglio dei due, scrivendo

i = i++

quello che stiamo veramente dicendo è "aggiungi 1 a i e assegna il risultato a i e assegna il risultato a i". Siamo confusi, quindi non mi dà troppo fastidio se anche il compilatore viene confuso.

Realisticamente, l'unica volta in cui queste espressioni folli vengono scritte è quando le persone le usano come esempi artificiali di come dovrebbe funzionare ++. E ovviamente è importante capire come funziona ++. Ma una regola pratica per usare ++ è: "Se non è ovvio che cosa significhi un'espressione che usa ++, non scriverlo".

Passavamo innumerevoli ore su comp.lang.c discutendo espressioni come queste e perché non sono definite. Due delle mie risposte più lunghe, che cercano di spiegare davvero il perché, sono archiviate sul Web:

Vedi anche mettere in discussione 3.8 e il resto delle domande nella sezione 3 della lista C FAQ .


1
Un piuttosto sgradevole Gotcha per quanto riguarda il comportamento non definito è che, mentre usato per essere sicuro su 99,9% di compilatori da utilizzare *p=(*q)++;per indicare if (p!=q) *p=(*q)++; else *p= __ARBITRARY_VALUE;che non è più il caso. Il C ipermoderno richiederebbe di scrivere qualcosa come quest'ultima formulazione (anche se non esiste un modo standard per indicare al codice che non importa cosa c'è dentro *p) per raggiungere il livello di efficienza utilizzato dai compilatori per fornire il primo (la elseclausola è necessaria per consentire il compilatore ottimizza ciò ifche alcuni compilatori più recenti richiederebbero).
supercat

@supercat Ora credo che qualsiasi compilatore che sia abbastanza "intelligente" per eseguire quel tipo di ottimizzazione deve anche essere abbastanza intelligente da sbirciare le assertdichiarazioni, in modo che il programmatore possa precedere la riga in questione con un semplice assert(p != q). (Naturalmente, prendendo quel corso richiederebbe anche la riscrittura <assert.h>di affermazioni non elimina a titolo definitivo nelle versioni non-debug, ma piuttosto, li trasformano in qualcosa di simile __builtin_assert_disabled()che il compilatore adeguata può vedere, e quindi non emettono codice per.)
Steve Summit

25

Spesso questa domanda è collegata come un duplicato di domande relative al codice come

printf("%d %d\n", i, i++);

o

printf("%d %d\n", ++i, i++);

o varianti simili.

Anche se questo è anche un comportamento indefinito come già affermato, ci sono sottili differenze quando printf()è coinvolto quando si confronta con un'affermazione come:

x = i++ + i++;

Nella seguente dichiarazione:

printf("%d %d\n", ++i, i++);

l' ordine di valutazione degli argomenti in nonprintf() è specificato . Ciò significa che le espressioni i++e ++ipotrebbero essere valutate in qualsiasi ordine. Lo standard C11 ha alcune descrizioni rilevanti al riguardo:

Allegato J, comportamenti non specificati

L'ordine in cui la designazione della funzione, gli argomenti e le sottoespressioni all'interno degli argomenti vengono valutati in una chiamata di funzione (6.5.2.2).

3.4.4, comportamento non specificato

Uso di un valore non specificato o altro comportamento in cui la presente norma internazionale offre due o più possibilità e non impone ulteriori requisiti su quale sia scelta in ogni caso.

ESEMPIO Un esempio di comportamento non specificato è l'ordine in cui vengono valutati gli argomenti di una funzione.

Lo stesso comportamento non specificato NON è un problema. Considera questo esempio:

printf("%d %d\n", ++x, y++);

Anche questo ha un comportamento non specificato perché l'ordine di valutazione di ++xe y++non è specificato. Ma è una dichiarazione perfettamente legale e valida. Non esiste un comportamento indefinito in questa affermazione. Perché le modifiche ( ++xe y++) vengono fatte su oggetti distinti .

Cosa rende la seguente dichiarazione

printf("%d %d\n", ++i, i++);

come comportamento indefinito è il fatto che queste due espressioni modificano lo stesso oggetto isenza un punto di sequenza intermedio .


Un altro dettaglio è che la virgola coinvolta nella chiamata printf () è un separatore , non l' operatore virgola .

Questa è una distinzione importante perché l' operatore virgola introduce un punto di sequenza tra la valutazione dei loro operandi, che rende legale quanto segue:

int i = 5;
int j;

j = (++i, i++);  // No undefined behaviour here because the comma operator 
                 // introduces a sequence point between '++i' and 'i++'

printf("i=%d j=%d\n",i, j); // prints: i=7 j=6

L'operatore virgola valuta i suoi operandi da sinistra a destra e restituisce solo il valore dell'ultimo operando. Quindi, in j = (++i, i++);, ++icon incrementi idi 6e i++rese vecchio valore di i( 6), che viene assegnato a j. Quindi idiventa 7dovuto al post-incremento.

Quindi, se la virgola nella chiamata di funzione dovesse essere un operatore virgola allora

printf("%d %d\n", ++i, i++);

non sarà un problema. Ma invoca un comportamento indefinito perché la virgola qui è un separatore .


Per coloro che sono nuovi al comportamento indefinito trarrebbero beneficio dalla lettura di ciò che ogni programmatore C dovrebbe sapere sul comportamento indefinito per comprendere il concetto e molte altre varianti del comportamento indefinito in C.

Questo post: è rilevante anche il comportamento non definito, non specificato e definito dall'implementazione .


Questa sequenza int a = 10, b = 20, c = 30; printf("a=%d b=%d c=%d\n", (a = a + b + c), (b = b + b), (c = c + c));sembra fornire un comportamento stabile (valutazione dell'argomento da destra a sinistra in gcc v7.3.0; risultato "a = 110 b = 40 c = 60"). È perché le assegnazioni sono considerate come "dichiarazioni complete" e introducono quindi un punto sequenza? Ciò non dovrebbe comportare una valutazione argomento / affermazione da sinistra a destra? Oppure è solo manifestazione di un comportamento indefinito?
kavadias,

@kavadias Quell'istruzione printf comporta un comportamento indefinito, per lo stesso motivo spiegato sopra. Stai scrivendo be rispettivamente cnel 3 ° e nel 4 ° argomento e leggi nel 2 ° argomento. Ma non c'è sequenza tra queste espressioni (2a, 3a e 4a discussione). gcc / clang ha un'opzione -Wsequence-pointche può aiutare a trovare anche questi.
PP,

23

Sebbene sia improbabile che qualsiasi compilatore e processore lo faccia effettivamente, sarebbe legale, secondo lo standard C, che il compilatore implementasse "i ++" con la sequenza:

In a single operation, read `i` and lock it to prevent access until further notice
Compute (1+read_value)
In a single operation, unlock `i` and store the computed value

Anche se non credo che nessun processore supporti l'hardware per consentire che una cosa del genere venga eseguita in modo efficiente, si può facilmente immaginare situazioni in cui tale comportamento renderebbe più semplice il codice multi-thread (ad esempio, garantirebbe che se due thread tentassero di eseguire quanto sopra sequenza simultanea, iverrebbe incrementato di due) e non è del tutto inconcepibile che un futuro processore possa fornire una funzionalità del genere.

Se il compilatore dovesse scrivere i++come indicato sopra (legale secondo lo standard) e intercalare le istruzioni di cui sopra durante la valutazione dell'espressione generale (anche legale), e se non fosse successo notare che è avvenuta una delle altre istruzioni per accedere i, sarebbe possibile (e legale) per il compilatore generare una sequenza di istruzioni che si sarebbero bloccate. A dire il vero, un compilatore quasi certamente rilevare il problema nel caso in cui la stessa variabile iviene utilizzato in entrambi i posti, ma se una routine accetta i riferimenti a due puntatori pe q, e gli usi (*p)e (*q)nella espressione di cui sopra (invece di usareidue volte) il compilatore non sarebbe tenuto a riconoscere o evitare il deadlock che si verificherebbe se lo stesso indirizzo dell'oggetto fosse passato per entrambi pe q.


16

Mentre la sintassi delle espressioni piace a = a++o a++ + a++è legale, il comportamento di questi costrutti è indefinito perché non si obbedisce a un must in C standard. C99 6.5p2 :

  1. Tra il punto di sequenza precedente e quello successivo, un oggetto deve avere il suo valore memorizzato modificato al massimo una volta dalla valutazione di un'espressione. [72] Inoltre, il valore precedente deve essere letto solo per determinare il valore da memorizzare [73]

Con la nota 73 si chiarisce ulteriormente ciò

  1. Questo paragrafo rende espressioni di istruzioni non definite come

    i = ++i + 1;
    a[i++] = i;

    pur permettendo

    i = i + 1;
    a[i] = i;

I vari punti di sequenza sono elencati nell'allegato C di C11 (e C99 ):

  1. Di seguito sono riportati i punti di sequenza descritti in 5.1.2.3:

    • Tra le valutazioni del designatore della funzione e gli argomenti effettivi in ​​una chiamata di funzione e la chiamata effettiva. (6.5.2.2).
    • Tra le valutazioni del primo e del secondo operando dei seguenti operatori: AND logico && (6.5.13); OR logico || (6.5.14); virgola, (6.5.17).
    • Tra le valutazioni del primo operando del condizionale? : operatore e qualunque sia il secondo e il terzo operando valutato (6.5.15).
    • La fine di un dichiaratore completo: dichiaratori (6.7.6);
    • Tra la valutazione di un'espressione completa e la successiva espressione completa da valutare. Le seguenti sono espressioni complete: un inizializzatore che non fa parte di un letterale composto (6.7.9); l'espressione in un'istruzione expression (6.8.3); l'espressione di controllo di un'istruzione di selezione (if o switch) (6.8.4); l'espressione di controllo di un'istruzione while o do (6.8.5); ciascuna delle espressioni (facoltative) di un'istruzione for (6.8.5.3); l'espressione (facoltativa) in un'istruzione return (6.8.6.4).
    • Immediatamente prima che ritorni una funzione di libreria (7.1.4).
    • Dopo le azioni associate a ciascun identificatore di conversione della funzione di input / output formattato (7.21.6, 7.29.2).
    • Immediatamente prima e immediatamente dopo ogni chiamata a una funzione di confronto, e anche tra qualsiasi chiamata a una funzione di confronto e qualsiasi movimento degli oggetti passati come argomenti a quella chiamata (7.22.5).

La formulazione dello stesso paragrafo in C11 è:

  1. Se un effetto collaterale su un oggetto scalare non è seguito rispetto a un diverso effetto collaterale sullo stesso oggetto scalare o a un calcolo del valore che utilizza il valore dello stesso oggetto scalare, il comportamento non è definito. Se sono presenti più ordini consentiti delle sottoespressioni di un'espressione, il comportamento non è definito se si verifica un tale effetto collaterale non seguito in uno qualsiasi degli ordini.84)

È possibile rilevare tali errori in un programma utilizzando ad esempio una versione recente di GCC con -Walle -Werror, quindi GCC rifiuterà completamente di compilare il programma. Di seguito è riportato l'output di gcc (Ubuntu 6.2.0-5ubuntu12) 6.2.0 20161005:

% gcc plusplus.c -Wall -Werror -pedantic
plusplus.c: In function main’:
plusplus.c:6:6: error: operation on i may be undefined [-Werror=sequence-point]
    i = i++ + ++i;
    ~~^~~~~~~~~~~
plusplus.c:6:6: error: operation on i may be undefined [-Werror=sequence-point]
plusplus.c:10:6: error: operation on i may be undefined [-Werror=sequence-point]
    i = (i++);
    ~~^~~~~~~
plusplus.c:14:6: error: operation on u may be undefined [-Werror=sequence-point]
    u = u++ + ++u;
    ~~^~~~~~~~~~~
plusplus.c:14:6: error: operation on u may be undefined [-Werror=sequence-point]
plusplus.c:18:6: error: operation on u may be undefined [-Werror=sequence-point]
    u = (u++);
    ~~^~~~~~~
plusplus.c:22:6: error: operation on v may be undefined [-Werror=sequence-point]
    v = v++ + ++v;
    ~~^~~~~~~~~~~
plusplus.c:22:6: error: operation on v may be undefined [-Werror=sequence-point]
cc1: all warnings being treated as errors

La parte importante è sapere cos'è un punto sequenza e cosa è un punto sequenza e cosa no . Ad esempio, l' operatore virgola è un punto di sequenza, quindi

j = (i ++, ++ i);

è ben definito e incrementerà idi uno, producendo il vecchio valore, scartandolo; quindi all'operatore virgola, regola gli effetti collaterali; e quindi incrementare idi uno, e il valore risultante diventa il valore dell'espressione - cioè questo è solo un modo inventato di scrivere j = (i += 2)che è ancora una volta un modo "intelligente" di scrivere

i += 2;
j = i;

Tuttavia, gli ,elenchi di argomenti nella funzione non sono un operatore virgola e non esiste un punto di sequenza tra le valutazioni di argomenti distinti; invece le loro valutazioni non sono seguite l'una rispetto all'altra; così la chiamata di funzione

int i = 0;
printf("%d %d\n", i++, ++i, i);

ha un comportamento indefinito perché non esiste un punto di sequenza tra le valutazioni di i++e ++inegli argomenti della funzione e il valore di iviene quindi modificato due volte, da entrambi i++e ++i, tra il punto di sequenza precedente e quello successivo.


14

Lo standard C afferma che una variabile dovrebbe essere assegnata al massimo una sola volta tra due punti di sequenza. Un punto e virgola, ad esempio, è un punto sequenza.
Quindi ogni affermazione del modulo:

i = i++;
i = i++ + ++i;

e così via viola questa regola. Lo standard afferma inoltre che il comportamento non è definito e non è specificato. Alcuni compilatori li rilevano e producono alcuni risultati, ma questo non è per standard.

Tuttavia, è possibile incrementare due diverse variabili tra due punti di sequenza.

while(*src++ = *dst++);

Quanto sopra è una pratica di codifica comune durante la copia / analisi delle stringhe.


Ovviamente non si applica a variabili diverse all'interno di un'espressione. Sarebbe un totale fallimento del progetto se lo facesse! Tutto ciò di cui hai bisogno nel secondo esempio è che entrambi siano incrementati tra la fine dell'istruzione e l'inizio successivo, e ciò è garantito, proprio a causa del concetto di punti sequenza al centro di tutto ciò.
underscore_d

11

In /programming/29505280/incrementing-array-index-in-c qualcuno ha chiesto di una dichiarazione del tipo:

int k[] = {0,1,2,3,4,5,6,7,8,9,10};
int i = 0;
int num;
num = k[++i+k[++i]] + k[++i];
printf("%d", num);

che stampa 7 ... l'OP si aspettava di stampare 6.

Gli ++iincrementi non sono garantiti per il completamento completo prima del resto dei calcoli. In effetti, diversi compilatori otterranno risultati diversi qui. Nell'esempio che hai fornito, il primo 2 ++ieseguita, quindi i valori di k[]sono stati letti, poi l'ultimo ++i, allora k[].

num = k[i+1]+k[i+2] + k[i+3];
i += 3

I compilatori moderni lo ottimizzeranno molto bene. In effetti, forse meglio del codice che hai scritto originariamente (supponendo che avesse funzionato come speravi).


5

Una spiegazione buona di ciò che accade in questo tipo di calcolo è fornita nel documento n1188 dal sito ISO W14 .

Spiego le idee.

La regola principale della norma ISO 9899 che si applica in questa situazione è 6.5p2.

Tra il punto di sequenza precedente e quello successivo, un oggetto deve avere il suo valore memorizzato modificato al massimo una volta dalla valutazione di un'espressione. Inoltre, il valore precedente deve essere letto solo per determinare il valore da memorizzare.

I punti di sequenza in un'espressione simile i=i++sono prima i=e dopo i++.

Nel documento che ho citato sopra viene spiegato che è possibile capire come il programma sia formato da piccole caselle, ciascuna contenente le istruzioni tra 2 punti consecutivi di sequenza. I punti di sequenza sono definiti nell'allegato C della norma, nel caso in cui i=i++vi siano 2 punti di sequenza che delimitano un'espressione completa. Tale espressione è sintatticamente equivalente con una voce expression-statementnella grammatica Backus-Naur (una grammatica è fornita nell'allegato A della norma).

Quindi l'ordine delle istruzioni all'interno di una scatola non ha un ordine chiaro.

i=i++

può essere interpretato come

tmp = i
i=i+1
i = tmp

o come

tmp = i
i = tmp
i=i+1

poiché entrambi questi moduli per interpretare il codice i=i++sono validi e poiché entrambi generano risposte diverse, il comportamento non è definito.

Quindi un punto di sequenza può essere visto all'inizio e alla fine di ogni riquadro che compone il programma [i riquadri sono unità atomiche in C] e all'interno di un riquadro l'ordine delle istruzioni non è definito in tutti i casi. Cambiando quell'ordine a volte si può cambiare il risultato.

MODIFICARE:

Un'altra buona fonte per spiegare tali ambiguità sono le voci dal sito c-faq (pubblicato anche come libro ), vale a dire qui e qui e qui .


In che modo questa risposta ha aggiunto nuove alle risposte esistenti? Anche le spiegazioni per i=i++sono molto simili a questa risposta .
Accade il

@haccks Non ho letto le altre risposte. Volevo spiegare nella mia lingua cosa ho imparato dal documento citato dal sito ufficiale della ISO 9899 open-std.org/jtc1/sc22/wg14/www/docs/n1188.pdf
alinsoar

5

Probabilmente la tua domanda non era: "Perché questi costrutti comportano un comportamento indefinito in C?". Probabilmente la tua domanda era "Perché questo codice (usando ++) non mi ha dato il valore che mi aspettavo?", E qualcuno ha contrassegnato la tua domanda come duplicata e ti ha inviato qui.

Questa risposta cerca di rispondere a quella domanda: perché il tuo codice non ti ha dato la risposta che ti aspettavi e come puoi imparare a riconoscere (ed evitare) espressioni che non funzioneranno come previsto.

Presumo che tu abbia già sentito la definizione di base di C ++e --operatori e di come il modulo prefisso ++xdifferisca dal modulo postfisso x++. Ma questi operatori sono difficili da pensare, quindi per essere sicuri di aver capito, forse hai scritto un piccolo programma di test che coinvolge qualcosa di simile

int x = 5;
printf("%d %d %d\n", x, ++x, x++);

Ma, con tua sorpresa, questo programma non ti ha aiutato a capire - ha stampato un output strano, inaspettato, inspiegabile, suggerendo che forse ++fa qualcosa di completamente diverso, non è affatto quello che pensavi avesse fatto.

O forse stai guardando un'espressione difficile da capire come

int x = 5;
x = x++ + ++x;
printf("%d\n", x);

Forse qualcuno ti ha dato quel codice come un puzzle. Anche questo codice non ha senso, specialmente se lo esegui - e se lo compili ed eseguilo con due compilatori diversi, avrai probabilmente due risposte diverse! Cosa succede con quello? Quale risposta è corretta? (E la risposta è che entrambi lo sono, o nessuno dei due lo è.)

Come hai sentito ormai, tutte queste espressioni sono indefinite , il che significa che il linguaggio C non fornisce alcuna garanzia su ciò che faranno. Questo è un risultato strano e sorprendente, perché probabilmente hai pensato che qualsiasi programma che potresti scrivere, purché compilato ed eseguito, genererebbe un output unico e ben definito. Ma nel caso di un comportamento indefinito, non è così.

Cosa rende indefinita un'espressione? Le espressioni coinvolgono ++e sono --sempre indefinite? Certo che no: si tratta di operatori utili e, se li usi correttamente, sono perfettamente definiti.

Per le espressioni di cui stiamo parlando, ciò che le rende indefinite è quando ci sono troppe cose in una volta, quando non siamo sicuri in quale ordine accadranno le cose, ma quando l'ordine conta per il risultato che otteniamo.

Torniamo ai due esempi che ho usato in questa risposta. Quando ho scritto

printf("%d %d %d\n", x, ++x, x++);

la domanda è, prima di chiamare printf, il compilatore calcola il valore di xfirst, o x++, o forse ++x? Ma risulta che non lo sappiamo . Non esiste una regola in C che dice che gli argomenti di una funzione vengono valutati da sinistra a destra, da destra a sinistra o in un altro ordine. Quindi non possiamo dire se il compilatore farà x, poi ++x, quindi x++, o x++allora ++xallora x, o qualche altro ordine. Ma l'ordine conta chiaramente, perché a seconda dell'ordine utilizzato dal compilatore, otterremo chiaramente risultati diversi stampati da printf.

Che mi dici di questa folle espressione?

x = x++ + ++x;

Il problema con questa espressione è che contiene tre diversi tentativi di modificare il valore di x: (1) la x++parte tenta di aggiungere 1 a x, memorizzare il nuovo valore xe restituire il vecchio valore di x; (2) la ++xparte tenta di aggiungere 1 a x, memorizza il nuovo valore xe restituisce il nuovo valore di x; e (3) la x =parte tenta di assegnare la somma degli altri due a x. Quale di questi tre tentativi di assegnazione "vincerà"? A quale dei tre valori verrà assegnato x? Ancora una volta, e forse sorprendentemente, in C non ci sono regole da dirci.

Potresti immaginare che la precedenza o l'associatività o la valutazione da sinistra a destra ti dicano in che ordine accadono le cose, ma non lo fanno. Potresti non credermi, ma per favore prendi la mia parola e la ripeterò: precedenza e associatività non determinano tutti gli aspetti dell'ordine di valutazione di un'espressione in C. In particolare, se all'interno di un'espressione ci sono più punti diversi in cui proviamo ad assegnare un nuovo valore a qualcosa come x, la precedenza e l'associatività non ci dicono quale di questi tentativi avvenga per primo, per ultimo o altro.


Quindi, con tutto quel background e introduzione fuori mano, se vuoi assicurarti che tutti i tuoi programmi siano ben definiti, quali espressioni puoi scrivere e quali non puoi scrivere?

Queste espressioni vanno tutte bene:

y = x++;
z = x++ + y++;
x = x + 1;
x = a[i++];
x = a[i++] + b[j++];
x[i++] = a[j++] + b[k++];
x = *p++;
x = *p++ + *q++;

Queste espressioni sono tutte indefinite:

x = x++;
x = x++ + ++x;
y = x + x++;
a[i] = i++;
a[i++] = i;
printf("%d %d %d\n", x, ++x, x++);

E l'ultima domanda è: come puoi dire quali espressioni sono ben definite e quali espressioni non sono definite?

Come ho detto prima, le espressioni indefinite sono quelle in cui ci sono troppe cose in una volta, dove non si può essere sicuri in quale ordine accadano le cose e dove l'ordine conta:

  1. Se esiste una variabile che viene modificata (assegnata a) in due o più posizioni diverse, come fai a sapere quale modifica si verifica per prima?
  2. Se c'è una variabile che viene modificata in un posto e che utilizza il suo valore in un altro posto, come fai a sapere se utilizza il vecchio valore o il nuovo valore?

Come esempio del n. 1, nell'espressione

x = x++ + ++x;

ci sono tre tentativi di modificare `x.

Come esempio del n. 2, nell'espressione

y = x + x++;

entrambi utilizziamo il valore xe lo modifichiamo.

Quindi questa è la risposta: assicurati che in ogni espressione che scrivi, ogni variabile venga modificata al massimo una volta e se una variabile viene modificata, non provi anche a usare il valore di quella variabile da qualche altra parte.


3

Il motivo è che il programma esegue un comportamento indefinito. Il problema risiede nell'ordine di valutazione, poiché non sono richiesti punti di sequenza in base allo standard C ++ 98 (nessuna operazione viene eseguita in sequenza prima o dopo un'altra secondo la terminologia C ++ 11).

Tuttavia, se ti attieni a un compilatore, troverai il comportamento persistente, purché non aggiungi chiamate di funzione o puntatori, il che renderebbe il comportamento più disordinato.

  • Quindi prima GCC: Usando Nuwen MinGW 15 GCC 7.1 otterrai:

    #include<stdio.h>
    int main(int argc, char ** argv)
    {
    int i = 0;
    i = i++ + ++i;
    printf("%d\n", i); // 2
    
    i = 1;
    i = (i++);
    printf("%d\n", i); //1
    
    volatile int u = 0;
    u = u++ + ++u;
    printf("%d\n", u); // 2
    
    u = 1;
    u = (u++);
    printf("%d\n", u); //1
    
    register int v = 0;
    v = v++ + ++v;
    printf("%d\n", v); //2

    }

Come funziona GCC? valuta le espressioni secondarie da un ordine da sinistra a destra per il lato destro (RHS), quindi assegna il valore al lato sinistro (LHS). Questo è esattamente il modo in cui Java e C # si comportano e definiscono i loro standard. (Sì, il software equivalente in Java e C # ha definito comportamenti). Valuta ciascuna sottoespressione una ad una nell'istruzione RHS in un ordine da sinistra a destra; per ogni sottoespressione: il ++ c (pre-incremento) viene valutato prima quindi il valore c viene utilizzato per l'operazione, quindi il post incremento c ++).

secondo GCC C ++: Operatori

In GCC C ++, la precedenza degli operatori controlla l'ordine in cui vengono valutati i singoli operatori

il codice equivalente nel comportamento definito C ++ come GCC comprende:

#include<stdio.h>
int main(int argc, char ** argv)
{
    int i = 0;
    //i = i++ + ++i;
    int r;
    r=i;
    i++;
    ++i;
    r+=i;
    i=r;
    printf("%d\n", i); // 2

    i = 1;
    //i = (i++);
    r=i;
    i++;
    i=r;
    printf("%d\n", i); // 1

    volatile int u = 0;
    //u = u++ + ++u;
    r=u;
    u++;
    ++u;
    r+=u;
    u=r;
    printf("%d\n", u); // 2

    u = 1;
    //u = (u++);
    r=u;
    u++;
    u=r;
    printf("%d\n", u); // 1

    register int v = 0;
    //v = v++ + ++v;
    r=v;
    v++;
    ++v;
    r+=v;
    v=r;
    printf("%d\n", v); //2
}

Quindi andiamo a Visual Studio . Visual Studio 2015, ottieni:

#include<stdio.h>
int main(int argc, char ** argv)
{
    int i = 0;
    i = i++ + ++i;
    printf("%d\n", i); // 3

    i = 1;
    i = (i++);
    printf("%d\n", i); // 2 

    volatile int u = 0;
    u = u++ + ++u;
    printf("%d\n", u); // 3

    u = 1;
    u = (u++);
    printf("%d\n", u); // 2 

    register int v = 0;
    v = v++ + ++v;
    printf("%d\n", v); // 3 
}

Come funziona Visual Studio, adotta un altro approccio, valuta tutte le espressioni di pre-incrementi nel primo passaggio, quindi utilizza i valori delle variabili nelle operazioni in secondo passaggio, assegna da RHS a LHS in terzo passaggio, quindi alla fine valuta tutto il espressioni post-incremento in un unico passaggio.

Quindi l'equivalente nel comportamento definito C ++ come Visual C ++ comprende:

#include<stdio.h>
int main(int argc, char ** argv)
{
    int r;
    int i = 0;
    //i = i++ + ++i;
    ++i;
    r = i + i;
    i = r;
    i++;
    printf("%d\n", i); // 3

    i = 1;
    //i = (i++);
    r = i;
    i = r;
    i++;
    printf("%d\n", i); // 2 

    volatile int u = 0;
    //u = u++ + ++u;
    ++u;
    r = u + u;
    u = r;
    u++;
    printf("%d\n", u); // 3

    u = 1;
    //u = (u++);
    r = u;
    u = r;
    u++;
    printf("%d\n", u); // 2 

    register int v = 0;
    //v = v++ + ++v;
    ++v;
    r = v + v;
    v = r;
    v++;
    printf("%d\n", v); // 3 
}

come indica la documentazione di Visual Studio in Precedenza e Ordine di valutazione :

Laddove diversi operatori compaiono insieme, hanno la stessa precedenza e vengono valutati in base alla loro associatività. Gli operatori nella tabella sono descritti nelle sezioni che iniziano con Postfix Operators.


1
Ho modificato la domanda per aggiungere l'UB nella valutazione degli argomenti della funzione, poiché questa domanda viene spesso utilizzata come duplicato per questo. (L'ultimo esempio)
Antti Haapala,

1
Anche la domanda riguarda c ora, non C ++
Antti Haapala,
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.