Comportamento non definito, non specificato e definito dall'implementazione


530

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?


1
Ero abbastanza sicuro di averlo fatto prima, ma non riesco a trovarlo. Vedi anche: stackoverflow.com/questions/2301372/...
dmckee --- ex-moderatore gattino



1
Ecco una discussione interessante (la sezione "Allegato L e comportamento indefinito").
Owen,

Risposte:


407

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 ppunta 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.


6
È un fatto strano che è derivato dall'unione che questa risposta copre solo C ++ ma i tag di questa domanda includono C. C ha una nozione diversa di "comportamento indefinito": richiederà comunque l'implementazione per fornire messaggi diagnostici anche se il comportamento è anche dichiarato a essere indefinito per determinate violazioni delle regole (violazioni dei vincoli).
Johannes Schaub - litb,

8
@Benoit È un comportamento indefinito perché lo standard dice che è un comportamento indefinito, punto. Su alcuni sistemi, infatti, i valori letterali di stringa sono memorizzati nel segmento di testo di sola lettura e il programma si arresta in modo anomalo se si tenta di modificare un valore letterale di stringa. Su altri sistemi, la stringa letterale apparirà effettivamente cambiata. Lo standard non prevede ciò che deve accadere. Questo è ciò che significa comportamento indefinito.
Fredoverflow,

5
@FredOverflow, Perché un buon compilatore ci consente di compilare codice che fornisce un comportamento indefinito? Esattamente quello buono può compilare questo tipo di codice dare? Perché tutti i buoni compilatori non ci hanno dato un enorme segnale di avvertimento rosso quando stiamo cercando di compilare un codice che dia un comportamento indefinito?
Pacerier,

14
@Pacerier Ci sono alcune cose che non sono controllabili al momento della compilazione. Ad esempio, non è sempre possibile garantire che un puntatore null non sia mai negato, ma questo non è definito.
Tim Seguine,

4
@Celeritas, il comportamento indefinito può essere non deterministico. Ad esempio, è impossibile sapere in anticipo quali saranno i contenuti della memoria non inizializzata, ad es. int f(){int a; return a;}: il valore di apuò cambiare tra le chiamate di funzione.
Segna il

97

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.


3
Qual è la differenza tra comportamento definito dall'implementazione e comportamento non specificato?
Zolomon

26
@Zolomon: Proprio come si dice: sostanzialmente la stessa cosa, tranne che in caso di definizione definita l'implementazione è richiesta per documentare (per garantire) cosa sta per accadere esattamente, mentre in caso non specificato l'implementazione non è richiesta per documentare o garantire qualcosa.
An

1
@Zolomon: si riflette nella differenza tra 3.4.1 e 2.4.4.
sabato

8
@Celeritas: i compilatori iper-moderni possono fare di meglio. Dato 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.
supercat,

2
@northerner Come afferma la citazione, il comportamento non specificato di solito è limitato a un insieme limitato di possibili comportamenti. In alcuni casi potresti persino giungere alla conclusione che tutte queste possibilità sono accettabili in un determinato contesto, in cui i comportamenti non specificati non sono affatto un problema. Il comportamento indefinito è completamente illimitato (eb "il programma potrebbe decidere di formattare il disco rigido"). Il comportamento indefinito è sempre un problema.
AnT

60

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 intnon 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.


1
perché fun(fun1(), fun2());non è il comportamento "implementation defined"? Il compilatore deve scegliere l'uno o l'altro corso, dopo tutto?
Lazer

1
@AraK: grazie per la spiegazione. Lo capisco adesso. A proposito, "I am gonna evaluate these arguments left-to-right and the next function's arguments are evaluated right-to-left"capisco che questo canaccada. Davvero, con i compilatori che usiamo in questi giorni?
Lazer

1
@eSKay Devi chiedere a un guru di questo che si è sporcato le mani con molti compilatori :) AFAIK VC valuta sempre gli argomenti da destra a sinistra.
AraK

4
@Lazer: può sicuramente succedere. Scenario semplice: foo (bar, boz ()) e foo (boz (), bar), dove bar è un int e boz () è una funzione che restituisce int. Supponiamo una CPU in cui si prevede che i parametri vengano passati nei registri R0-R1. I risultati delle funzioni vengono restituiti in R0; le funzioni possono eliminare R1. Valutare "bar" prima di "boz ()" richiederebbe il salvataggio di una copia della barra da qualche altra parte prima di chiamare boz () e quindi caricare quella copia salvata. La valutazione di "bar" dopo "boz ()" eviterà l'archiviazione e il recupero della memoria, ed è un'ottimizzazione che molti compilatori farebbero indipendentemente dal loro ordine nell'elenco degli argomenti.
supercat

6
Non conosco C ++, ma lo standard C afferma che una conversione di un int in un carattere è definita dall'implementazione o addirittura ben definita (a seconda dei valori effettivi e della firma dei tipi). Vedi C99 §6.3.1.3 (invariato in C11).
Nikolai Ruhe,

27

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.


3
Gli autori di compilatori iper-moderni considerano anche il "comportamento indefinito" come una concessione agli autori di compilatori la licenza di presumere che i programmi non riceveranno mai input che potrebbero causare comportamenti indefiniti e di modificare arbitrariamente tutti gli aspetti del comportamento dei programmi quando ricevono tali input.
supercat

2
Un altro punto che ho appena notato: C89 non ha usato il termine "estensione" per descrivere funzionalità garantite su alcune implementazioni ma non su altre. Gli autori di C89 hanno riconosciuto che la maggior parte delle implementazioni attuali avrebbe trattato identicamente l'aritmetica firmata e l'aritmetica non firmata, tranne quando i risultati sono stati utilizzati in alcuni modi e tale trattamento è stato applicato anche in caso di overflow firmato; tuttavia non lo elencarono come un'estensione comune dell'allegato J2, il che mi suggerisce di vederlo come una situazione naturale, piuttosto che un'estensione.
supercat,

10

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.


1
Esistono due tipi di compilatori: quelli che, se non diversamente documentato in modo esplicito, interpretano la maggior parte delle forme di comportamento indefinito dello Standard come ricadute su comportamenti caratteristici documentati dall'ambiente sottostante, e quelli che per impostazione predefinita espongono solo in modo utile comportamenti che lo Standard definisce come implementazione definita. Quando si utilizzano compilatori del primo tipo, molte cose del primo tipo possono essere eseguite in modo efficiente e sicuro utilizzando UB. I compilatori per il secondo tipo saranno adatti a tali compiti solo se forniscono opzioni per garantire il comportamento in tali casi.
supercat

8

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 << powcome uno spostamento del complemento a due, indipendentemente dal vfatto che sia positivo o negativo.

La filosofia preferita tra alcuni degli autori di compilatori di oggi, tuttavia, suggerirebbe che, poiché vpuò 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.


Ma gestire un comportamento indefinito in un modo carino non è gratuito. L'intera ragione per cui i compilatori moderni mostrano un comportamento così bizzarro in alcuni casi di UB è che stanno ottimizzando incessantemente e, per fare il miglior lavoro, devono essere in grado di supporre che UB non si verifichi mai.
Tom Swirly,

Ma il fatto che <<sia UB su numeri negativi è una cattiva trappola e sono contento di ricordarmelo!
Tom Swirly,

1
@TomSwirly: Sfortunatamente, gli autori di compilatori non si preoccupano del fatto che offrire garanzie comportamentali indipendenti oltre a quelle previste dalla norma può spesso consentire un aumento di velocità enorme rispetto alla necessità che il codice eviti a tutti i costi qualsiasi cosa non definita dalla norma. Se a un programmatore non importa se i+j>kproduce 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.
supercat,

1
@TomSwirly: per loro, se il compilatore X può prendere un programma rigorosamente conforme per svolgere un compito T e produrre un eseguibile che è il 5% più efficiente di quello che il compilatore Y produrrebbe con quello stesso programma, ciò significa che X è migliore, anche se Y potrebbe generare codice che ha svolto la stessa attività tre volte in modo efficiente dato un programma che sfrutta comportamenti che Y garantisce ma X no.
supercat

6

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.


6

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.


2
Penso che sia importante notare che il significato pratico di "indefinito" è cambiato negli ultimi anni. Una volta era quello dato uint32_t s;, valutando che 1u<<squando sci 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<<spossono indurre un compilatore a determinare che, poiché in precedenza sdeve essere stato meno di 32, qualsiasi codice prima o dopo quell'espressione che sarebbe rilevante solo se sfosse stato 32 o maggiore può essere omesso.
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.