In quale punto del ciclo l'overflow dell'intero diventa un comportamento indefinito?


86

Questo è un esempio per illustrare la mia domanda che coinvolge un codice molto più complicato che non posso pubblicare qui.

#include <stdio.h>
int main()
{
    int a = 0;
    for (int i = 0; i < 3; i++)
    {
        printf("Hello\n");
        a = a + 1000000000;
    }
}

Questo programma contiene un comportamento indefinito sulla mia piattaforma perché aandrà in overflow al 3 ° ciclo.

Questo fa sì che l' intero programma abbia un comportamento indefinito o solo dopo che l' overflow si è effettivamente verificato ? Il compilatore potrebbe potenzialmente risolvere che a andrà in overflow in modo da poter dichiarare l'intero ciclo indefinito e non preoccuparsi di eseguire printfs anche se si verificano tutti prima dell'overflow?

(Contrassegnati C e C ++ anche se sono diversi perché sarei interessato alle risposte per entrambe le lingue se sono diverse.)


7
Chissà se il compilatore potrebbe risolvere che anon è utilizzato (tranne che per il calcolo stesso) e rimuoverlo semplicementea
4386427

12
Potresti divertirti con My Little Optimizer: Undefined Behavior is Magic di CppCon quest'anno. Riguarda le ottimizzazioni che i compilatori possono eseguire in base a un comportamento indefinito.
TartanLlama



Risposte:


108

Se sei interessato a una risposta puramente teorica, lo standard C ++ consente un comportamento indefinito al "viaggio nel tempo":

[intro.execution]/5: Un'implementazione conforme che esegue un programma ben formato produrrà lo stesso comportamento osservabile di una delle possibili esecuzioni dell'istanza corrispondente della macchina astratta con lo stesso programma e lo stesso input. Tuttavia, se una qualsiasi di tali esecuzioni contiene un'operazione indefinita, la presente norma internazionale non impone alcun requisito all'implementazione che esegue quel programma con quell'input (nemmeno per quanto riguarda le operazioni che precedono la prima operazione indefinita)

Pertanto, se il tuo programma contiene un comportamento indefinito, il comportamento dell'intero programma non è definito.


4
@KeithThompson: Ma poi, la sneeze()funzione stessa è indefinita su qualsiasi cosa della classe Demon(di cui la varietà nasale è una sottoclasse), rendendo comunque l'intera cosa circolare.
Sebastian Lenartowicz

1
Ma printf potrebbe non tornare, quindi i primi due round sono definiti perché fino a quando non vengono eseguiti non è chiaro che ci sarà mai UB. Vedi stackoverflow.com/questions/23153445/…
usr

1
Questo è il motivo per cui un compilatore è tecnicamente nei suoi diritti di emettere "nop" per il kernel Linux (perché il codice bootstrap si basa su un comportamento indefinito): blog.regehr.org/archives/761
Crashworks

3
@Crashworks Ed è per questo che Linux è scritto e compilato come C. non portabile (cioè un superset di C che richiede un particolare compilatore con opzioni particolari, come -fno-strict-aliasing)
user253751

3
@usr Mi aspetto che sia definito se printfnon ritorna, ma se printfritorna, allora il comportamento non definito può causare problemi prima che printfvenga chiamato. Quindi, viaggio nel tempo. printf("Hello\n");e quindi la riga successiva viene compilata comeundoPrintf(); launchNuclearMissiles();
user253751

31

Innanzitutto, consentitemi di correggere il titolo di questa domanda:

Undefined Behavior non è (specificamente) del regno dell'esecuzione.

Il comportamento indefinito influisce su tutti i passaggi: compilazione, collegamento, caricamento ed esecuzione.

Alcuni esempi per cementarlo, tieni presente che nessuna sezione è esaustiva:

  • il compilatore può presumere che le parti di codice che contengono un comportamento indefinito non vengano mai eseguite, e quindi presumere che i percorsi di esecuzione che li porterebbero siano codice morto. Vedi quello che ogni programmatore C dovrebbe sapere sul comportamento indefinito nientemeno che da Chris Lattner.
  • il linker può presumere che in presenza di più definizioni di un simbolo debole (riconosciuto per nome), tutte le definizioni siano identiche grazie alla One Definition Rule
  • il caricatore (nel caso si utilizzino librerie dinamiche) può assumere lo stesso, selezionando così il primo simbolo che trova; questo è solitamente (ab) usato per intercettare le chiamate usando LD_PRELOADtrucchi su Unix
  • l'esecuzione potrebbe non riuscire (SIGSEV) se si utilizzano puntatori pendenti

Questo è ciò che fa così paura del comportamento indefinito: è quasi impossibile prevedere, in anticipo, quale sarà il comportamento esatto e questa previsione deve essere rivista ad ogni aggiornamento della toolchain, del sistema operativo sottostante, ...


Consiglio di guardare questo video di Michael Spencer (sviluppatore LLVM): CppCon 2016: My Little Optimizer: Undefined Behavior is Magic .


3
Questo è ciò che mi preoccupa. Nel mio codice reale, è complesso ma potrei avere un caso in cui sarà sempre traboccante. E non mi interessa davvero, ma temo che anche il codice "corretto" ne sarà influenzato. Ovviamente ho bisogno di aggiustarlo, ma il fissaggio richiede comprensione :)
jcoder

8
@jcoder: c'è una via di fuga importante qui. Il compilatore non è autorizzato a indovinare i dati di input. Finché c'è almeno un input per il quale non si verifica un comportamento indefinito, il compilatore deve assicurarsi che questo particolare input produca ancora l'output corretto. Tutti i discorsi spaventosi sulle ottimizzazioni pericolose si applicano solo all'inevitabile UB. In pratica, se avessi usato argccome conteggio loop, il caso argc=1non produce UB e il compilatore sarebbe costretto a gestirlo.
MSalters

@jcoder: in questo caso, questo non è un codice morto. Il compilatore, tuttavia, potrebbe essere abbastanza intelligente da dedurre che inon può essere incrementato più di Nvolte e quindi che il suo valore è limitato.
Matthieu M.

4
@jcoder: Se f(good);fa qualcosa X e f(bad);invoca un comportamento indefinito, allora un programma che invoca solo f(good);è garantito per fare X, ma f(good); f(bad);non è garantito che faccia X.

4
@Hurkyl più interessante, se il tuo codice è if(foo) f(good); else f(bad);, un compilatore intelligente getterà via il confronto e produrrà un file incondizionato foo(good).
John Dvorak

28

Un ottimizzando aggressivamente C o C ++ mira un po '16 intsi sa che il comportamento su come aggiungere 1000000000a un inttipo è indefinito .

È consentito da entrambi gli standard di fare tutto ciò che vuole che potrebbe includere la cancellazione dell'intero programma, lasciando int main(){}.

Ma per quanto riguarda le ints più grandi ? Non conosco un compilatore che lo faccia ancora (e non sono un esperto nella progettazione di compilatori C e C ++ in alcun modo), ma immagino che a volte un compilatore che punta a 32 bit into superiore capirà che il ciclo è infinito ( inon cambia) e quindi afinirà per traboccare. Quindi, ancora una volta, può ottimizzare l'output in int main(){}. Il punto che sto cercando di sottolineare qui è che man mano che le ottimizzazioni del compilatore diventano progressivamente più aggressive, costrutti di comportamento sempre più indefiniti si manifestano in modi inaspettati.

Il fatto che il tuo ciclo sia infinito non è di per sé indefinito poiché stai scrivendo sullo standard output nel corpo del ciclo.


3
È consentito dallo standard fare tutto ciò che vuole anche prima che il comportamento indefinito si manifesti? Dove viene affermato?
jimifiki

4
perché 16 bit? Immagino che OP stia cercando un overflow con segno a 32 bit.
4386427

8
@jimifiki Nello standard. C ++ 14 (N4140) 1.3.24 "comportamento udnefined = comportamento per il quale questo standard internazionale non impone requisiti." Più una lunga nota che elabora. Ma il punto è che non è il comportamento di una "dichiarazione" che è indefinito, è il comportamento del programma. Ciò significa che finché UB è attivato da una regola nello standard (o dall'assenza di una regola), lo standard cessa di essere applicato al programma nel suo insieme. Quindi qualsiasi parte del programma può comportarsi come vuole.
Angew non è più orgoglioso di SO

5
La prima affermazione è sbagliata. Se intè a 16 bit, l'aggiunta avverrà in long(perché l'operando letterale ha un tipo long) dove è ben definito, quindi verrà riconvertita da una conversione definita dall'implementazione in int.
R .. GitHub SMETTA DI AIUTARE IL GHIACCIO

2
@usr il comportamento di printfè definito dallo standard per restituire sempre
MM

11

Tecnicamente, secondo lo standard C ++, se un programma contiene un comportamento indefinito, il comportamento dell'intero programma, anche in fase di compilazione (prima ancora che il programma venga eseguito), è indefinito.

In pratica, poiché il compilatore può presumere (come parte di un'ottimizzazione) che l'overflow non si verificherà, almeno il comportamento del programma alla terza iterazione del ciclo (assumendo una macchina a 32 bit) sarà indefinito, sebbene è probabile che otterrai risultati corretti prima della terza iterazione. Tuttavia, poiché il comportamento dell'intero programma è tecnicamente indefinito, non c'è nulla che impedisca al programma di generare un output completamente errato (incluso nessun output), che si arresti in modo anomalo in fase di esecuzione in qualsiasi momento durante l'esecuzione, o addirittura non riesca a compilare del tutto (poiché il comportamento indefinito si estende a tempo di compilazione).

Il comportamento indefinito fornisce al compilatore più spazio per l'ottimizzazione perché eliminano alcuni presupposti su ciò che il codice deve fare. In tal modo, non è garantito che i programmi che si basano su presupposti che implicano un comportamento indefinito funzionino come previsto. Pertanto, non dovresti fare affidamento su alcun comportamento particolare considerato indefinito dallo standard C ++.


Cosa succede se la parte UB rientra in un if(false) {}ambito? Avvelena l'intero programma, a causa del fatto che il compilatore presume che tutti i rami contengano parti ben definite di logica e quindi opera su presupposti sbagliati?
mlvljr

1
Lo standard non impone alcun requisito a un comportamento indefinito, quindi in teoria sì, avvelena l'intero programma. Tuttavia, in pratica , qualsiasi compilatore di ottimizzazione probabilmente rimuoverà semplicemente il codice inattivo, quindi probabilmente non avrebbe alcun effetto sull'esecuzione. Tuttavia, non dovresti ancora fare affidamento su questo comportamento.
bwDraco

Buono a sapersi, grazie :)
mlvljr

9

Per capire perché un comportamento indefinito può "viaggiare nel tempo", come ha detto in modo adeguato @TartanLlama , diamo un'occhiata alla regola "come se":

1.9 Esecuzione del programma

1 Le descrizioni semantiche in questo standard internazionale definiscono una macchina astratta non deterministica parametrizzata. La presente norma internazionale non pone alcun requisito sulla struttura delle implementazioni conformi. In particolare, non hanno bisogno di copiare o emulare la struttura della macchina astratta. Piuttosto, sono necessarie implementazioni conformi per emulare (solo) il comportamento osservabile della macchina astratta come spiegato di seguito.

Con questo, potremmo vedere il programma come una "scatola nera" con un input e un output. L'input potrebbe essere l'input dell'utente, i file e molte altre cose. L'output è il "comportamento osservabile" menzionato nello standard.

Lo standard definisce solo una mappatura tra l'input e l'output, nient'altro. Lo fa descrivendo una "scatola nera di esempio", ma dice esplicitamente che qualsiasi altra scatola nera con la stessa mappatura è ugualmente valida. Ciò significa che il contenuto della scatola nera è irrilevante.

Con questo in mente, non avrebbe senso dire che un comportamento indefinito si verifica in un determinato momento. Nell'implementazione di esempio della scatola nera, potremmo dire dove e quando accade, ma l' effettivo scatola nera potrebbe essere qualcosa di completamente diverso, quindi non possiamo più dire dove e quando accade. Teoricamente, un compilatore potrebbe, ad esempio, decidere di enumerare tutti i possibili input e pre-calcolare gli output risultanti. Quindi il comportamento indefinito si sarebbe verificato durante la compilazione.

Il comportamento indefinito è l'inesistenza di una mappatura tra input e output. Un programma può avere un comportamento indefinito per alcuni input, ma un comportamento definito per altri. Quindi la mappatura tra input e output è semplicemente incompleta; c'è un input per il quale non esiste alcuna mappatura all'output.
Il programma nella domanda ha un comportamento indefinito per qualsiasi input, quindi la mappatura è vuota.


6

Supponendo che intsia a 32 bit, il comportamento indefinito si verifica alla terza iterazione. Quindi, se, ad esempio, il ciclo fosse raggiungibile solo in modo condizionale o potesse essere terminato in modo condizionale prima della terza iterazione, non ci sarebbe alcun comportamento indefinito a meno che la terza iterazione non venga effettivamente raggiunta. Tuttavia, in caso di comportamento indefinito, tutto l'output del programma è indefinito, compreso l'output che è "nel passato" relativo all'invocazione di un comportamento indefinito. Ad esempio, nel tuo caso, questo significa che non c'è alcuna garanzia di vedere 3 messaggi "Hello" nell'output.


6

La risposta di TartanLlama è corretta. Il comportamento indefinito può verificarsi in qualsiasi momento, anche durante la fase di compilazione. Può sembrare assurdo, ma è una caratteristica fondamentale per consentire ai compilatori di fare ciò che devono fare. Non è sempre facile essere un compilatore. Devi fare esattamente quello che dice la specifica, ogni volta. Tuttavia, a volte può essere mostruosamente difficile dimostrare che si sta verificando un determinato comportamento. Se ricordi il problema dell'arresto, è piuttosto banale sviluppare software per il quale non puoi provare se completa o entra in un ciclo infinito quando alimentato con un particolare input.

Potremmo fare in modo che i compilatori siano pessimisti e compilino costantemente per paura che la prossima istruzione possa essere uno di questi problemi di arresto come i problemi, ma questo non è ragionevole. Diamo invece un passaggio al compilatore: su questi argomenti di "comportamento indefinito", sono liberati da ogni responsabilità. Il comportamento indefinito consiste in tutti i comportamenti che sono così sottilmente nefasti che abbiamo difficoltà a separarli dai problemi di arresto veramente brutti e nefasti e quant'altro.

C'è un esempio che mi piace postare, anche se ammetto di aver perso la fonte, quindi devo parafrasare. Proveniva da una particolare versione di MySQL. In MySQL, avevano un buffer circolare riempito con i dati forniti dall'utente. Ovviamente volevano assicurarsi che i dati non superassero il buffer, quindi hanno avuto un controllo:

if (currentPtr + numberOfNewChars > endOfBufferPtr) { doOverflowLogic(); }

Sembra abbastanza sano. Tuttavia, cosa succede se numberOfNewChars è davvero grande e trabocca? Quindi si avvolge e diventa un puntatore più piccolo di endOfBufferPtr, quindi la logica di overflow non verrebbe mai chiamata. Quindi hanno aggiunto un secondo controllo, prima di quello:

if (currentPtr + numberOfNewChars < currentPtr) { detectWrapAround(); }

Sembra che tu ti sia preso cura dell'errore di overflow del buffer, giusto? Tuttavia, è stato presentato un bug affermando che questo buffer era in overflow su una particolare versione di Debian! Un'attenta analisi ha mostrato che questa versione di Debian è stata la prima a utilizzare una versione particolarmente avanzata di gcc. In questa versione di gcc, il compilatore ha riconosciuto che currentPtr + numberOfNewChars non può mai essere un puntatore più piccolo di currentPtr perché l'overflow per i puntatori è un comportamento indefinito! Questo è stato sufficiente per gcc per ottimizzare l'intero controllo, e improvvisamente non sei stato protetto dai buffer overflow anche se hai scritto il codice per controllarlo!

Questo era un comportamento specifico. Tutto era legale (anche se da quello che ho sentito, gcc ha annullato questa modifica nella versione successiva). Non è quello che considererei un comportamento intuitivo, ma se allunghi un po 'la tua immaginazione, è facile vedere come una leggera variante di questa situazione potrebbe diventare un problema di arresto per il compilatore. Per questo motivo, gli autori delle specifiche lo hanno definito "Undefined Behavior" e hanno affermato che il compilatore poteva fare assolutamente qualsiasi cosa gli piacesse.


Non considero compilatori particolarmente sorprendenti che a volte si comportano come se l'aritmetica con segno fosse eseguita su tipi il cui intervallo si estende oltre "int", soprattutto considerando che anche quando si fa la generazione di codice semplice su x86 ci sono momenti in cui farlo è più efficiente del troncamento intermedio risultati. Cosa c'è di più sorprendente è quando di overflow colpisce altri calcoli, che possono accadere in gcc, anche se i negozi di codice il prodotto di due valori uint16_t in un uint32_t - operazione che dovrebbe avere alcun motivo plausibile per agire sorprendente in un accumulo non igienizzante.
supercat

Ovviamente, il controllo corretto sarebbe if(numberOfNewChars > endOfBufferPtr - currentPtr), ammesso che numberOfNewChars non possa mai essere negativo e currentPtr punti sempre a qualche punto all'interno del buffer non è nemmeno necessario il ridicolo controllo "avvolgente". (Non credo che il codice che hai fornito abbia alcuna speranza di funzionare in un buffer circolare - hai tralasciato tutto ciò che è necessario per quello nella parafrasi, quindi sto ignorando anche quel caso)
Random832

@ Random832 Ho lasciato fuori un sacco. Ho provato a citare il contesto più ampio, ma da quando ho perso la mia fonte, ho scoperto che parafrasare il contesto mi ha messo più nei guai, quindi lo tralascio. Ho davvero bisogno di trovare quella maledetta segnalazione di bug in modo da poterla citare correttamente. È davvero un potente esempio di come puoi pensare di aver scritto il codice in un modo e farlo compilare in modo completamente diverso.
Cort Ammon

Questo è il mio problema più grande con un comportamento indefinito. A volte rende impossibile scrivere codice corretto e, quando il compilatore lo rileva, di default non ti dice che ha attivato un comportamento indefinito. In questo caso l'utente vuole semplicemente fare aritmetica - puntatore o meno - e tutto il loro duro lavoro per scrivere codice sicuro è stato annullato. Ci dovrebbe almeno essere un modo per annotare una sezione di codice per dire: nessuna ottimizzazione di fantasia qui. C / C ++ viene utilizzato in troppe aree critiche per consentire a questa situazione pericolosa di continuare a favore dell'ottimizzazione
John McGrath

4

Al di là delle risposte teoriche, un'osservazione pratica sarebbe che per molto tempo i compilatori hanno applicato varie trasformazioni sui cicli per ridurre la quantità di lavoro svolto al loro interno. Ad esempio, dato:

for (int i=0; i<n; i++)
  foo[i] = i*scale;

un compilatore potrebbe trasformarlo in:

int temp = 0;
for (int i=0; i<n; i++)
{
  foo[i] = temp;
  temp+=scale;
}

Salvando così una moltiplicazione ad ogni iterazione del ciclo. Un'ulteriore forma di ottimizzazione, che i compilatori adattati con vari gradi di aggressività, la trasformerebbe in:

if (n > 0)
{
  int temp1 = n*scale;
  int *temp2 = foo;
  do
  {
    temp1 -= scale;
    *temp2++ = temp1;
  } while(temp1);
}

Anche su macchine con avvolgimento silenzioso su overflow, ciò potrebbe non funzionare correttamente se ci fosse un numero inferiore a n che, moltiplicato per scala, restituirebbe 0. Potrebbe anche trasformarsi in un ciclo infinito se la scala fosse letta dalla memoria più di una volta e qualcosa ha cambiato il suo valore in modo imprevisto (in ogni caso dove "scale" potrebbe cambiare a metà ciclo senza invocare UB, un compilatore non sarebbe autorizzato a eseguire l'ottimizzazione).

Sebbene la maggior parte di tali ottimizzazioni non avrebbe alcun problema nei casi in cui due tipi brevi senza segno vengono moltiplicati per produrre un valore compreso tra INT_MAX + 1 e UINT_MAX, gcc ha alcuni casi in cui una tale moltiplicazione all'interno di un ciclo può causare l'uscita anticipata del ciclo . Non ho notato tali comportamenti derivanti dalle istruzioni di confronto nel codice generato, ma è osservabile nei casi in cui il compilatore utilizza l'overflow per dedurre che un ciclo può essere eseguito al massimo 4 volte o meno; per impostazione predefinita non genera avvisi nei casi in cui alcuni input causerebbero UB e altri no, anche se le sue inferenze fanno sì che il limite superiore del ciclo venga ignorato.


4

Il comportamento indefinito è, per definizione, un'area grigia. Semplicemente non puoi prevedere cosa farà o non farà - questo è ciò che significa "comportamento indefinito" .

Da tempo immemorabile, i programmatori hanno sempre cercato di recuperare i resti di definizione da una situazione indefinita. Hanno un po 'di codice realmente vogliono usare, ma che si rivela essere non definito, in modo da cercare di argomentare: "Lo so che è indefinito, ma sicuramente saranno, nel peggiore dei casi, fare questo o questo, ma non potrà mai fare che . " E a volte questi argomenti sono più o meno giusti, ma spesso sono sbagliati. E man mano che i compilatori diventano sempre più intelligenti (o, alcuni potrebbero dire, più subdoli e subdoli), i confini della domanda continuano a cambiare.

Quindi, davvero, se vuoi scrivere codice che è garantito per funzionare e che continuerà a funzionare per molto tempo, c'è solo una scelta: evitarti il ​​comportamento indefinito a tutti i costi. In verità, se ci diletti, tornerà a perseguitarti.


eppure, ecco il punto ... i compilatori possono usare un comportamento indefinito per ottimizzare ma IN GENERALE NON TE LO DICONO. Quindi, se abbiamo questo fantastico strumento che devi evitare di fare X a tutti i costi, perché il compilatore non può darti un avvertimento in modo che tu possa aggiustarlo?
Jason S

1

Una cosa che il tuo esempio non considera è l'ottimizzazione. aè impostato nel ciclo ma non viene mai utilizzato e un ottimizzatore potrebbe risolverlo. In quanto tale, è legittimo che l'ottimizzatore scartare acompletamente, e in quel caso tutti i comportamenti indefiniti svaniscono come la vittima di un boojum.

Tuttavia, ovviamente questo stesso non è definito, perché l'ottimizzazione è indefinita. :)


1
Non c'è motivo di considerare l'ottimizzazione quando si determina se il comportamento è indefinito.
Keith Thompson

2
Il fatto che il programma si comporti come si potrebbe supporre che dovrebbe non significa che il comportamento indefinito "svanisca". Il comportamento è ancora indefinito e ti affidi semplicemente alla fortuna. Il fatto stesso che il comportamento del programma possa cambiare in base alle opzioni del compilatore è un forte indicatore del fatto che il comportamento non è definito.
Jordan Melo

@JordanMelo Poiché molte delle risposte precedenti hanno discusso l'ottimizzazione (e l'OP ha chiesto specificamente di questo), ho menzionato una caratteristica dell'ottimizzazione che nessuna risposta precedente aveva coperto. Ho anche sottolineato che anche se l'ottimizzazione potrebbe rimuoverlo, fare affidamento sull'ottimizzazione per funzionare in un modo particolare è ancora indefinito. Non lo sto certo raccomandando! :)
Graham

@KeithThompson Certo, ma l'OP ha chiesto specificamente dell'ottimizzazione e del suo effetto sul comportamento indefinito che avrebbe visto sulla sua piattaforma. Quel comportamento specifico potrebbe svanire, a seconda dell'ottimizzazione. Come ho detto nella mia risposta, però, l'indefinizione no.
Graham

0

Poiché questa domanda è doppiamente etichettata C e C ++, cercherò di affrontare entrambi. C e C ++ adottano approcci diversi qui.

In C l'implementazione deve essere in grado di dimostrare che il comportamento indefinito sarà invocato per trattare l'intero programma come se avesse un comportamento indefinito. Nell'esempio OP sembrerebbe banale per il compilatore dimostrarlo e quindi è come se l'intero programma fosse indefinito.

Possiamo vederlo da Defect Report 109 che al suo punto cruciale chiede:

Se, tuttavia, lo standard C riconosce l'esistenza separata di "valori indefiniti" (la cui semplice creazione non implica del tutto "un comportamento indefinito"), una persona che esegue test del compilatore potrebbe scrivere un caso di test come il seguente, e potrebbe anche aspettarsi (o possibilmente richiedere) che un'implementazione conforme dovrebbe, per lo meno, compilare questo codice (e possibilmente anche consentirne l'esecuzione) senza "fallire".

int array1[5];
int array2[5];
int *p1 = &array1[0];
int *p2 = &array2[0];

int foo()
{
int i;
i = (p1 > p2); /* Must this be "successfully translated"? */
1/0; /* Must this be "successfully translated"? */
return 0;
}

Quindi la domanda di fondo è questa: il codice sopra deve essere "tradotto con successo" (qualunque cosa significhi)? (Vedere la nota a piè di pagina allegata al sottopunto 5.1.1.3.)

e la risposta è stata:

Lo standard C usa il termine "valore indeterminato" non "valore indefinito". L'uso di un oggetto di valore indeterminato produce un comportamento indefinito. La nota a piè di pagina del sottopunto 5.1.1.3 indica che un'implementazione è libera di produrre un numero qualsiasi di diagnostica fintanto che un programma valido è ancora tradotto correttamente. Se un'espressione la cui valutazione risulterebbe in un comportamento indefinito appare in un contesto in cui è richiesta un'espressione costante, il programma contenitore non è strettamente conforme. Inoltre, se ogni possibile esecuzione di un dato programma risulterebbe in un comportamento indefinito, il dato programma non è strettamente conforme. Un'implementazione conforme non deve mancare di tradurre un programma strettamente conforme semplicemente perché una possibile esecuzione di quel programma risulterebbe in un comportamento indefinito. Poiché foo potrebbe non essere mai chiamato, l'esempio fornito deve essere tradotto con successo da un'implementazione conforme.

In C ++ l'approccio sembra più rilassato e suggerirebbe che un programma abbia un comportamento indefinito indipendentemente dal fatto che l'implementazione possa dimostrarlo staticamente o meno.

Abbiamo [intro.abstrac] p5 che dice:

Un'implementazione conforme che esegue un programma ben formato produrrà lo stesso comportamento osservabile di una delle possibili esecuzioni dell'istanza corrispondente della macchina astratta con lo stesso programma e lo stesso input. Tuttavia, se tale esecuzione contiene un'operazione non definita, questo documento non pone alcun requisito sull'implementazione che esegue quel programma con quell'input (nemmeno per quanto riguarda le operazioni che precedono la prima operazione non definita).


Il fatto che l'esecuzione di una funzione richiami UB può influenzare il modo in cui un programma si comporta quando viene fornito un input particolare se almeno una possibile esecuzione del programma quando viene fornito quell'input richiama UB. Il fatto che l'invocazione di una funzione richiami UB non impedisce a un programma di avere un comportamento definito quando riceve un input che non consentirebbe di richiamare la funzione.
supercat

@supercat Credo che sia ciò che la mia risposta ci dice almeno per C.
Shafik Yaghmour

Penso che lo stesso valga per il testo citato in C ++, poiché la frase "Qualsiasi esecuzione di questo tipo" si riferisce ai modi in cui il programma potrebbe essere eseguito con un determinato input. Se un particolare input non può provocare l'esecuzione di una funzione, non vedo nulla nel testo citato per dire che qualsiasi cosa in tale funzione risulterebbe in UB.
supercat

-2

La risposta principale è un'idea sbagliata (ma comune):

Il comportamento indefinito è una proprietà di runtime *. E NON PUO "viaggio nel tempo"!

Alcune operazioni sono definite (dallo standard) per avere effetti collaterali e non possono essere ottimizzate. Le operazioni che eseguono operazioni di I / O o che accedono a volatilevariabili rientrano in questa categoria.

Tuttavia , c'è un avvertimento: UB può essere qualsiasi comportamento, incluso il comportamento che annulla le operazioni precedenti. Ciò può avere conseguenze simili, in alcuni casi, all'ottimizzazione del codice precedente.

In effetti, questo è coerente con la citazione nella risposta in alto (enfasi mia):

Un'implementazione conforme che esegue un programma ben formato produrrà lo stesso comportamento osservabile di una delle possibili esecuzioni dell'istanza corrispondente della macchina astratta con lo stesso programma e lo stesso input.
Tuttavia, se una di tali esecuzioni contiene un'operazione indefinita, la presente norma internazionale non impone alcun requisito all'implementazione che esegue quel programma con quell'input (nemmeno per quanto riguarda le operazioni che precedono la prima operazione non definita).

Sì, questa citazione non dire "nemmeno per quanto riguarda le operazioni che precedono la prima operazione indefinito" , a meno di notare che questo è specificamente sul codice che viene eseguito , non semplicemente compilato.
Dopotutto, un comportamento indefinito che non è effettivamente raggiunto non fa nulla, e affinché la riga contenente UB sia effettivamente raggiunta, il codice che lo precede deve essere eseguito per primo!

Quindi sì, una volta eseguito UB , qualsiasi effetto delle operazioni precedenti diventa indefinito. Ma fino a quando ciò non accade, l'esecuzione del programma è ben definita.

Notare, tuttavia, che tutte le esecuzioni del programma che provocano questo accadimento possono essere ottimizzate per programmi equivalenti , comprese quelle che eseguono operazioni precedenti ma poi annullano i loro effetti. Di conseguenza, il codice precedente può essere ottimizzato ogni volta che farlo equivarrebbe all'annullamento dei loro effetti ; altrimenti non può. Vedi sotto per un esempio.

* Nota: questo non è in contrasto con UB che si verifica in fase di compilazione . Se il compilatore può effettivamente provare che il codice UB verrà sempre eseguito per tutti gli input, allora UB può estendersi al tempo di compilazione. Tuttavia, ciò richiede la consapevolezza che tutto il codice precedente alla fine ritorna , il che è un requisito fondamentale. Di nuovo, vedi sotto per un esempio / spiegazione.


Per rendere questo concreto, nota che il codice seguente deve essere stampato fooe attendere il tuo input indipendentemente da qualsiasi comportamento non definito che lo segue:

printf("foo");
getchar();
*(char*)1 = 1;

Tuttavia, nota anche che non c'è alcuna garanzia che foorimarrà sullo schermo dopo che si è verificato l'UB o che il carattere che hai digitato non sarà più nel buffer di input; entrambe queste operazioni possono essere "annullate", il che ha un effetto simile al "viaggio nel tempo" di UB.

Se la getchar()linea non fosse presente, sarebbe legale che le linee venissero ottimizzate se e solo se ciò fosse indistinguibile dall'emissione fooe quindi "annullamento".

Il fatto che i due siano indistinguibili o meno dipenderà interamente dall'implementazione (cioè dal compilatore e dalla libreria standard). Ad esempio, puoi printf bloccare il tuo thread qui mentre aspetti che un altro programma legga l'output? O tornerà immediatamente?

  • Se può bloccare qui, allora un altro programma può rifiutarsi di leggere il suo output completo, e potrebbe non tornare mai più, e di conseguenza UB potrebbe non verificarsi mai.

  • Se può tornare immediatamente qui, allora sappiamo che deve tornare, e quindi ottimizzarlo è del tutto indistinguibile dall'eseguirlo e quindi annullare i suoi effetti.

Naturalmente, poiché il compilatore sa quale comportamento è consentito per la sua particolare versione di printf, può ottimizzare di conseguenza e di conseguenza printfpuò essere ottimizzato in alcuni casi e non in altri. Ma, ancora una volta, la giustificazione è che questo sarebbe indistinguibile dall'annullamento di operazioni precedenti da parte di UB, non che il codice precedente sia "avvelenato" a causa di UB.


1
Stai completamente fraintendendo lo standard. Dice che il comportamento durante l'esecuzione del programma non è definito. Periodo. Questa risposta è sbagliata al 100%. Lo standard è molto chiaro: l'esecuzione di un programma con input che produce UB in qualsiasi punto del flusso di esecuzione ingenuo non è definito.
David Schwartz

@DavidSchwartz: se segui la tua interpretazione fino alle sue conclusioni logiche, dovresti renderti conto che non ha senso logico. L'input non è qualcosa che è completamente determinato all'avvio del programma. L'input al programma (anche la sua semplice presenza ) in una data riga può dipendere da tutti gli effetti collaterali del programma fino a quella riga. Pertanto, il programma non può evitare di produrre gli effetti collaterali che vengono prima della linea UB, perché ciò richiede l'interazione con il suo ambiente e quindi influisce sul fatto che la linea UB verrà raggiunta o meno in primo luogo.
user541686

3
Non importa. Veramente. Ancora una volta, ti manca solo l'immaginazione. Ad esempio, se il compilatore può dire che nessun codice conforme potrebbe dire la differenza, potrebbe spostare il codice che è UB in modo tale che la parte che è UB venga eseguita prima degli output che ingenuamente ti aspetti che siano "precedenti".
David Schwartz,

2
@Mehrdad: Forse un modo migliore per dire le cose sarebbe dire che UB non può viaggiare indietro nel tempo oltre l'ultimo punto in cui sarebbe potuto accadere qualcosa nel mondo reale che avrebbe definito il comportamento. Se un'implementazione potesse determinare esaminando i buffer di input che non c'era modo che nessuna delle successive 1000 chiamate a getchar () potesse bloccarsi, e potesse anche determinare che UB si sarebbe verificato dopo la millesima chiamata, non sarebbe necessario eseguire nessuna delle le chiamate. Se, tuttavia, un'implementazione dovesse specificare che l'esecuzione non passerà un getchar () fino a quando tutto l'output precedente non avesse ...
supercat

2
... è stato consegnato a un terminale a 300 baud, e che qualsiasi control-C che si verifica prima di questo farà sì che getchar () sollevi un segnale anche se ci fossero altri caratteri nel buffer che lo precedono, allora tale implementazione non potrebbe sposta qualsiasi UB oltre l'ultimo output che precede getchar (). Ciò che è difficile è sapere in quale caso ci si dovrebbe aspettare che un compilatore passi attraverso il programmatore qualsiasi garanzia comportamentale che un'implementazione della libreria potrebbe offrire oltre a quelle richieste dallo Standard.
supercat
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.