Qual è il comportamento indefinito in C e C ++? Che dire del comportamento non specificato e del comportamento definito dall'implementazione? Qual'è la differenza tra loro?
Qual è il comportamento indefinito in C e C ++? Che dire del comportamento non specificato e del comportamento definito dall'implementazione? Qual'è la differenza tra loro?
Risposte:
Il comportamento indefinito è uno di quegli aspetti del linguaggio C e C ++ che può sorprendere i programmatori provenienti da altre lingue (altre lingue cercano di nasconderlo meglio). Fondamentalmente, è possibile scrivere programmi C ++ che non si comportano in modo prevedibile, anche se molti compilatori C ++ non segnaleranno alcun errore nel programma!
Diamo un'occhiata a un classico esempio:
#include <iostream>
int main()
{
char* p = "hello!\n"; // yes I know, deprecated conversion
p[0] = 'y';
p[5] = 'w';
std::cout << p;
}
La variabile p
punta alla stringa letterale "hello!\n"
e le due assegnazioni sottostanti tentano di modificare quella stringa letterale. Cosa fa questo programma? Secondo la sezione 2.14.5 paragrafo 11 della norma C ++, invoca un comportamento indefinito :
L'effetto del tentativo di modificare una stringa letterale non è definito.
Riesco a sentire le persone urlare "Ma aspetta, non posso compilare questo problema e ottenere l'output yellow
" o "Che cosa vuoi dire indefinito, i letterali delle stringhe sono archiviati in memoria di sola lettura, quindi il primo tentativo di assegnazione si traduce in un dump principale". Questo è esattamente il problema con un comportamento indefinito. Fondamentalmente, lo standard consente qualsiasi cosa accada una volta invocato un comportamento indefinito (anche i demoni nasali). Se esiste un comportamento "corretto" secondo il tuo modello mentale della lingua, quel modello è semplicemente sbagliato; Lo standard C ++ ha l'unico voto, punto.
Altri esempi di comportamento indefinito includono l'accesso a un array oltre i suoi limiti, la dereferenziazione del puntatore null , l' accesso agli oggetti al termine della loro durata o la scrittura di espressioni presumibilmente intelligenti come i++ + ++i
.
La sezione 1.9 della norma C ++ menziona anche i due fratelli meno pericolosi del comportamento indefinito, il comportamento non specificato e comportamento definito dall'implementazione :
Le descrizioni semantiche di questo standard internazionale definiscono una macchina astratta non deterministica parametrizzata.
Alcuni aspetti e operazioni della macchina astratta sono descritti nella presente norma internazionale come definiti dall'implementazione (ad esempio
sizeof(int)
). Questi costituiscono i parametri della macchina astratta. Ciascuna implementazione deve includere documentazione che descriva le sue caratteristiche e il suo comportamento sotto questi aspetti.Alcuni altri aspetti e operazioni della macchina astratta sono descritti nella presente norma internazionale come non specificati (ad esempio, l'ordine di valutazione degli argomenti di una funzione). Laddove possibile, questo standard internazionale definisce un insieme di comportamenti consentiti. Questi definiscono gli aspetti non deterministici della macchina astratta.
Alcune altre operazioni sono descritte in questo standard internazionale come indefinite (ad esempio, l'effetto di dereferenziare il puntatore null). [ Nota : questo standard internazionale non impone requisiti sul comportamento dei programmi che contengono comportamenti indefiniti. - nota finale ]
In particolare, la sezione 1.3.24 afferma:
Il comportamento non definito consentito 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), alla conclusione di una traduzione o esecuzione (con l'emissione di un messaggio diagnostico).
Cosa puoi fare per evitare di imbatterti in comportamenti indefiniti? Fondamentalmente, devi leggere buoni libri in C ++ di autori che sanno di cosa stanno parlando. Vite tutorial su Internet. Bullschildt di vite.
int f(){int a; return a;}
: il valore di a
può cambiare tra le chiamate di funzione.
Bene, questo è fondamentalmente un semplice copia-incolla dallo standard
3.4.1 1 comportamento definito dall'implementazione non specificato in cui ogni implementazione documenta come viene effettuata la scelta
2 ESEMPIO Un esempio di comportamento definito dall'implementazione è la propagazione del bit di ordine superiore quando un intero con segno viene spostato a destra.
3.4.3 1 comportamento indefinito comportamentale , 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 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).
3 ESEMPIO Un esempio di comportamento indefinito è il comportamento sull'overflow di numeri interi.
3.4.4 1 comportamento non specificato uso di un valore non specificato o altro comportamento in cui la presente norma internazionale prevede due o più possibilità e non impone ulteriori requisiti sui quali viene scelta in ogni caso
2 ESEMPIO Un esempio di comportamento non specificato è l'ordine in cui vengono valutati gli argomenti di una funzione.
int foo(int x) { if (x >= 0) launch_missiles(); return x << 1; }
un compilatore può determinare che, poiché tutti i mezzi per invocare la funzione che non avviano i missili invocano il comportamento indefinito, può rendere la chiamata launch_missiles()
incondizionata.
Forse una formulazione semplice potrebbe essere più semplice da comprendere rispetto alla rigorosa definizione degli standard.
comportamento definito dall'implementazione
Il linguaggio dice che abbiamo tipi di dati. I fornitori del compilatore specificano quali dimensioni devono usare e forniscono una documentazione di ciò che hanno fatto.
comportamento indefinito
Stai facendo qualcosa di sbagliato. Ad esempio, hai un valore molto grande in un int
non adatto char
. Come si inserisce quel valore char
? in realtà non c'è modo! Tutto potrebbe succedere, ma la cosa più sensata sarebbe prendere il primo byte di quell'int e inserirlo char
. È solo sbagliato farlo assegnando il primo byte, ma è quello che succede sotto il cofano.
comportamento non specificato
Quale funzione di questi due viene eseguita per prima?
void fun(int n, int m);
int fun1()
{
cout << "fun1";
return 1;
}
int fun2()
{
cout << "fun2";
return 2;
}
...
fun(fun1(), fun2()); // which one is executed first?
La lingua non specifica la valutazione, da sinistra a destra o da destra a sinistra! Quindi un comportamento non specificato può o meno comportare un comportamento indefinito, ma certamente il tuo programma non dovrebbe produrre un comportamento non specificato.
@eSKay Penso che la tua domanda valga la pena modificare la risposta per chiarire di più :)
perché
fun(fun1(), fun2());
il comportamento "implementazione definita" non è? Il compilatore deve scegliere l'uno o l'altro corso, dopo tutto?
La differenza tra definizione definita e non specificata è che il compilatore dovrebbe scegliere un comportamento nel primo caso, ma non è necessario nel secondo caso. Ad esempio, un'implementazione deve avere una e una sola definizione di sizeof(int)
. Quindi, non si può dire che sizeof(int)
sia 4 per una parte del programma e 8 per altri. A differenza del comportamento non specificato, in cui il compilatore può dire OK, valuterò questi argomenti da sinistra a destra e gli argomenti della funzione successiva verranno valutati da destra a sinistra. Può succedere nello stesso programma, ecco perché si chiama non specificato . In effetti, C ++ avrebbe potuto essere semplificato se fossero stati specificati alcuni comportamenti non specificati. Dai un'occhiata qui alla risposta del Dr. Stroustrup per questo :
Si sostiene che la differenza tra ciò che può essere prodotto dando al compilatore questa libertà e richiedendo una "valutazione ordinaria da sinistra a destra" può essere significativa. Non sono convinto, ma con innumerevoli compilatori "là fuori" che sfruttano la libertà e alcune persone che difendono appassionatamente quella libertà, un cambiamento sarebbe difficile e potrebbe richiedere decenni per penetrare negli angoli distanti dei mondi C e C ++. Sono deluso dal fatto che non tutti i compilatori mettono in guardia contro codici come ++ i + i ++. Allo stesso modo, l'ordine di valutazione degli argomenti non è specificato.
Troppe "cose" dell'IMO sono lasciate indefinite, non specificate, definite dall'implementazione, ecc. Tuttavia, è facile da dire e persino da fornire esempi, ma difficili da risolvere. Va anche notato che non è poi così difficile evitare la maggior parte dei problemi e produrre codice portatile.
fun(fun1(), fun2());
non è il comportamento "implementation defined"
? Il compilatore deve scegliere l'uno o l'altro corso, dopo tutto?
"I am gonna evaluate these arguments left-to-right and the next function's arguments are evaluated right-to-left"
capisco che questo can
accada. Davvero, con i compilatori che usiamo in questi giorni?
Dal documento ufficiale di motivazione
I termini comportamento non specificato , comportamento indefinito e comportamento definito dall'implementazione vengono utilizzati per classificare il risultato della scrittura di programmi le cui proprietà lo Standard non descrive o non può descrivere completamente. L'obiettivo dell'adozione di questa categorizzazione è consentire una certa varietà tra le implementazioni che consentono alla qualità dell'implementazione di essere una forza attiva sul mercato, nonché consentire alcune estensioni popolari, senza rimuovere la cache di conformità allo Standard. L'Appendice F allo Standard cataloga quei comportamenti che rientrano in una di queste tre categorie.
Il comportamento non specificato dà all'implementatore un certo margine di manovra nella traduzione dei programmi. Questa latitudine non si estende fino a quando non riesce a tradurre il programma.
Comportamenti indefiniti danno al responsabile dell'attuazione la licenza di non rilevare alcuni errori del programma che sono difficili da diagnosticare. Identifica anche aree di possibile estensione della lingua conforme: l'implementatore può aumentare la lingua fornendo una definizione del comportamento ufficialmente indefinito.
Il comportamento definito dall'implementazione offre a un implementatore la libertà di scegliere l'approccio appropriato, ma richiede che questa scelta sia spiegata all'utente. I comportamenti designati come definiti dall'implementazione sono generalmente quelli in cui un utente può prendere decisioni di codifica significative basate sulla definizione dell'implementazione. Gli attuatori dovrebbero tenere presente questo criterio quando decidono quanto estesa dovrebbe essere una definizione di implementazione. Come nel caso di comportamenti non specificati, la semplice mancata traduzione della fonte contenente il comportamento definito dall'implementazione non è una risposta adeguata.
Il comportamento indefinito contro il comportamento non specificato ne ha una breve descrizione.
Il loro riassunto finale:
Per riassumere, il comportamento non specificato è di solito qualcosa di cui non dovresti preoccuparti, a meno che il tuo software non sia richiesto per essere portatile. Al contrario, un comportamento indefinito è sempre indesiderabile e non dovrebbe mai verificarsi.
Storicamente, sia il comportamento definito dall'implementazione che il comportamento indefinito rappresentavano situazioni in cui gli autori dello standard si aspettavano che le persone che scrivevano implementazioni di qualità avrebbero usato il giudizio per decidere quali garanzie comportamentali, se del caso, sarebbero state utili per i programmi nel campo dell'applicazione previsto in esecuzione sul obiettivi previsti. Le esigenze del codice di crunching dei numeri di fascia alta sono abbastanza diverse da quelle del codice di sistemi di basso livello, e sia UB che IDB offrono agli scrittori di compilatori la flessibilità di soddisfare queste diverse esigenze. Nessuna delle due categorie impone che le implementazioni si comportino in modo utile per uno scopo particolare o anche per qualsiasi scopo. Le implementazioni di qualità che dichiarano di essere adatte a uno scopo particolare, tuttavia, dovrebbero comportarsi in modo adeguato a tale scopose lo standard lo richiede o meno .
L'unica differenza tra il comportamento definito dall'implementazione e il comportamento indefinito è che il primo richiede che le implementazioni definiscano e documentino un comportamento coerente anche nei casi in cui nulla dell'implementazione potrebbe essere utile . La linea di demarcazione tra di loro non è se sarebbe generalmente utile per le implementazioni definire comportamenti (gli autori di compilatori dovrebbero definire comportamenti utili quando pratico se lo Standard li richiede o meno) ma se ci potrebbero essere implementazioni in cui la definizione di un comportamento sarebbe contemporaneamente costosa e inutile . Un giudizio sull'esistenza di tali implementazioni non implica in alcun modo, forma o forma, alcun giudizio sull'utilità di supportare un comportamento definito su altre piattaforme.
Sfortunatamente, dalla metà degli anni '90 gli autori di compilatori hanno iniziato a interpretare la mancanza di mandati comportamentali come un giudizio secondo cui le garanzie comportamentali non valgono il costo anche nei campi di applicazione in cui sono vitali e persino nei sistemi in cui non costano praticamente nulla. Invece di considerare UB come un invito a esercitare un giudizio ragionevole, gli autori di compilatori hanno iniziato a trattarlo come una scusa per non farlo.
Ad esempio, dato il seguente codice:
int scaled_velocity(int v, unsigned char pow)
{
if (v > 250)
v = 250;
if (v < -250)
v = -250;
return v << pow;
}
un'implementazione del complemento a due non dovrebbe fare alcuno sforzo per trattare l'espressione v << pow
come uno spostamento del complemento a due, indipendentemente dal v
fatto che sia positivo o negativo.
La filosofia preferita tra alcuni degli autori di compilatori di oggi, tuttavia, suggerirebbe che, poiché v
può essere negativo solo se il programma si impegnerà in un comportamento indefinito, non c'è motivo per avere il programma di tagliare l'intervallo negativo di v
. Anche se lo spostamento a sinistra dei valori negativi era supportato su ogni singolo compilatore di significatività e una grande quantità di codice esistente si basa su quel comportamento, la filosofia moderna interpreterebbe il fatto che lo Standard afferma che i valori negativi a spostamento a sinistra sono UB come sottintendendo che gli autori di compilatori dovrebbero sentirsi liberi di ignorarlo.
<<
sia UB su numeri negativi è una cattiva trappola e sono contento di ricordarmelo!
i+j>k
produce 1 o 0 nei casi in cui l'aggiunta trabocca, purché non abbia altri effetti collaterali , un compilatore potrebbe essere in grado di apportare alcune ottimizzazioni di massa che non sarebbero possibili se il programmatore scrivesse il codice come (int)((unsigned)i+j) > k
.
Standard C ++ n3337 § 1.3.10 comportamento definito dall'implementazione
comportamento, per un programma ben strutturato costruire e correggere i dati, che dipende dall'implementazione e che ogni documento di implementazione
A volte lo standard C ++ non impone un comportamento particolare su alcuni costrutti ma dice invece che un particolare comportamento ben definito deve essere scelto e descritto da un'implementazione particolare (versione della libreria). Quindi l'utente può ancora sapere esattamente come si comporterà il programma anche se Standard non lo descrive.
C ++ standard n3337 § 1.3.24 comportamento indefinito
comportamento per il quale questo standard internazionale non impone requisiti [Nota: un comportamento indefinito può essere previsto quando questo standard internazionale omette qualsiasi definizione esplicita di comportamento o quando un programma utilizza un costrutto errato o dati errati. Il comportamento non definito consentito 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), alla conclusione di una traduzione o esecuzione (con l'emissione di un messaggio diagnostico). Molti costrutti di programma errati non generano comportamenti indefiniti; devono essere diagnosticati. - nota finale]
Quando il programma incontra un costrutto che non è definito in base allo standard C ++, gli è permesso fare tutto ciò che vuole fare (magari mandandomi un'e-mail o magari inviarti un'e-mail o ignorando completamente il codice).
C ++ standard n3337 § 1.3.25 comportamento non specificato
comportamento, per un costrutto di programma ben formato e dati corretti, che dipende dall'implementazione [Nota: l'implementazione non è richiesta per documentare quale comportamento si verifica. La gamma di possibili comportamenti è di solito delineata da questo standard internazionale. - nota finale]
Lo standard C ++ non impone un comportamento particolare su alcuni costrutti ma dice invece che un particolare comportamento ben definito deve essere scelto ( bot non necessario descritto ) da una particolare implementazione (versione della libreria). Quindi, nel caso in cui non sia stata fornita alcuna descrizione, può essere difficile per l'utente sapere esattamente come si comporterà il programma.
Implementazione definita
Gli implementatori desiderano, dovrebbero essere ben documentati, lo standard offre delle scelte ma sicuramente da compilare
Non specificato -
Come definito dall'implementazione ma non documentato
Non definito-
Tutto potrebbe succedere, prenditi cura di esso.
uint32_t s;
, valutando che 1u<<s
quando s
ci si aspettava 33 poteva forse produrre 0 o forse produrre 2, ma non fare nient'altro di strano. I compilatori più recenti, tuttavia, durante la valutazione 1u<<s
possono indurre un compilatore a determinare che, poiché in precedenza s
deve essere stato meno di 32, qualsiasi codice prima o dopo quell'espressione che sarebbe rilevante solo se s
fosse stato 32 o maggiore può essere omesso.