Comportamento indefinito, in linea di principio

8

Sia in C che in C ++, penso che questo programma illegale, il cui comportamento secondo lo standard C o C ++ non è definito, sia interessante:

#include <stdio.h>

int foo() {
    int a;
    const int b = a;
    a = 555;
    return b;
}

void bar() {
    int x = 123;
    int y = 456;
}

int main() {
    bar();
    const int n1 = foo();
    const int n2 = foo();
    const int n3 = foo();
    printf("%d %d %d\n", n1, n2, n3);
    return 0;
}

Output sulla mia macchina (dopo la compilazione senza ottimizzazione):

123 555 555

Penso che questo programma illegale sia interessante perché illustra la meccanica dello stack, perché la ragione per cui si usa C o C ++ (invece di, per esempio, Java) è programmare vicino all'hardware, vicino alla meccanica dello stack e simili.

Tuttavia, su StackOverflow, quando il codice di un interrogatore inavvertitamente legge dallo storage non inizializzato, le risposte con maggiore scalabilità citano invariabilmente lo standard C o C ++ (specialmente C ++) per l'effetto che il comportamento non è definito. Questo è vero, naturalmente, per quanto riguarda gli standard - il comportamento è davvero indefinito - ma è curioso che risposte alternative provino, da un punto di vista hardware o stack-meccanico, per indagare sul perché specifico il comportamento non definito (come l'output sopra) potrebbe essersi verificato, è raro e tende ad essere ignorato.

Ricordo persino una risposta che suggeriva che comportamento indefinito potrebbe includere la riformattazione del mio disco rigido. Non mi sono preoccupato troppo di questo, però, prima di eseguire il programma sopra.

La mia domanda è questa: Perché è più importante insegnare ai lettori semplicemente che il comportamento non è definito in C o C ++, piuttosto che capire il comportamento indefinito? Voglio dire, se il lettore capisce il comportamento non definito, quindi non sarebbe il più probabile per evitarlo?

La mia istruzione è in ingegneria elettrotecnica e lavoro come ingegnere edile e l'ultima volta che ho avuto un lavoro come programmatore per sé è stato nel 1994, quindi sono curioso di comprendere la prospettiva degli utenti con sfondi di sviluppo software più convenzionali e più recenti.

    
posta thb 13.09.2014 - 03:12
fonte

5 risposte

5

L'analisi del valore di Frama-C, un analizzatore statico il cui obiettivo dichiarato è quello di trovare tutti i comportamenti non definiti in un programma C, considera l'assegnazione const int b = a; come corretta. Questa è una decisione progettuale intenzionale al fine di consentire memcpy() (tipicamente implementato come un loop su unsigned char elementi di un array virtuale, e che lo standard C consente di ri-implementare come tale) per copiare un struct ( che può avere padding e membri non inizializzati) a un altro.

L''"eccezione" è solo per lvalue = lvalue; assegnazioni senza una conversione intermedia, ovvero per un incarico che equivale a una copia di una porzione di memoria per una posizione di memoria su un'altra.

I (come uno degli autori dell'analisi del valore di Frama-C) discusso di questo con Xavier Leroy in un momento in cui si chiedeva se la definizione prendesse il compilatore C CompCert verificato, quindi potrebbe aver finito per usare il stessa definizione. È a mio avviso più pulito di quello che lo standard C cerca di fare con valori indeterminati che possono essere rappresentazioni di trap e il tipo unsigned char che è garantito non avere alcuna rappresentazione di trap, ma sia CompCert che Frama-C assumono relativamente non- obiettivi esotici e forse il comitato di standardizzazione stava cercando di ospitare piattaforme in cui la lettura di un int non inizializzato può effettivamente interrompere il programma.

Restituire b , o passare n1 , n2 o n3 a printf alla fine almeno può essere considerato un comportamento non definito, perché la copia di una porzione di memoria non inizializzata non la rende inizializzata. Con una versione Frama-C obsoleta:

$ frama-c -val t.c
…
t.c:19:… accessing uninitialized left-value: assert \initialized(&n1);

E in una versione obsoleta di CompCert, dopo piccole modifiche per rendere accettabile il programma:

$ ccomp -interp t.c
Time 33: in function foo, expression <loc> = <undef>
ERROR: Undefined behavior
    
risposta data 13.09.2014 - 03:26
fonte
7

Il comportamento non definito alla fine significa che il comportamento non è deterministico. I programmatori che non sono consapevoli di scrivere codice non deterministico sono solo cattivi programmatori ignoranti. Questo sito mira a rendere i programmatori migliori (e meno ignoranti).

Scrivere un programma corretto di fronte a comportamenti non deterministici non è impossibile. Tuttavia, è un ambiente di programmazione specializzato e richiede un diverso tipo di disciplina di programmazione.

Anche nel tuo esempio, se il programma riceve un segnale alzato esternamente, i valori sullo "stack" potrebbero cambiare in modo tale da non ottenere i valori previsti. Inoltre, se la macchina ha valori di trappola, leggere valori casuali può benissimo far succedere qualcosa di strano.

    
risposta data 13.09.2014 - 03:29
fonte
4

"Comportamento indefinito" è una scorciatoia per "Questo comportamento non è deterministico, non solo si comporterà probabilmente diversamente in diversi compilatori o piattaforme hardware, ma potrebbe anche comportarsi diversamente in versioni diverse dello stesso compilatore."

La maggior parte dei programmatori considererebbe questa caratteristica indesiderabile, specialmente dal momento che C e C ++ sono linguaggi basati su standard ; cioè, li usi, in parte, perché le specifiche del linguaggio forniscono certe garanzie su come si comporterà la lingua, se stai usando un compilatore conforme agli standard.

Come per la maggior parte delle cose nella programmazione, devi valutare i vantaggi e gli svantaggi. Se il beneficio di qualche operazione che è UB supera la difficoltà di farlo funzionare in modo stabile, indipendente dalla piattaforma, allora con tutti i mezzi, usa il comportamento indefinito. La maggior parte dei programmatori penserà che non ne valga la pena, il più delle volte.

Il rimedio per qualsiasi comportamento indefinito è esaminare il comportamento che effettivamente ottieni, data una particolare piattaforma e compilatore. Quella specie di esame non è quella che un programmatore esperto è in grado di esplorare per te in un'impostazione di Q & A.

    
risposta data 13.09.2014 - 23:40
fonte
4

Why is it more important to teach readers merely that behavior is undefined in C or C++, than it is to understand the undefined behavior?

Poiché il comportamento specifico potrebbe non essere ripetibile, anche da esecuzione a esecuzione senza ricostruzione.

Inseguire esattamente quello che è successo può essere un utile esercizio accademico per capire meglio le peculiarità della tua piattaforma particolare, ma da una prospettiva di coding l'unica lezione pertinente è "non farlo". Un'espressione come a++ * a++ è un errore di codifica, punto e basta. Questo è davvero tutto quello che ha bisogno di per sapere.

    
risposta data 18.09.2014 - 00:58
fonte
1

Se la documentazione per un particolare compilatore dice cosa farà quando il codice fa qualcosa che è considerato "comportamento indefinito" dallo standard, allora il codice che si basa su quel comportamento funzionerà correttamente quando compilato con quel compilatore , ma può comportarsi in modo arbitrario una volta compilato usando un altro compilatore la cui documentazione non specifica il comportamento.

Se la documentazione per un compilatore non specifica come gestirà un particolare "comportamento indefinito", il fatto che il comportamento di un programma sembri obbedire a certe regole dice nulla su come si comportano i programmi simili . Qualsiasi varietà di fattori può far sì che un compilatore emetta un codice che gestisce le situazioni inaspettate in modo diverso - a volte in modo apparentemente bizzarro.

Consideriamo, ad esempio, su una macchina in cui int è un numero intero a 32 bit:

int undef_behavior_example(uint16_t size1, uint16_t size2)
{
  int flag = 0;
  if ((uint32_t)size1 * size2 > 2147483647u)
    flag += 1;
  if (((size1*size2) & 127) != 0) // Test whether product is a multiple of 128
    flag += 2;
  return flag;
}

Se size1 e size2 erano entrambi uguali a 46341 (il loro prodotto è 2147488281) ci si potrebbe aspettare che la funzione restituisca 3, ma un compilatore può legittimamente saltare interamente il primo test; o il prodotto sarebbe abbastanza piccolo da rendere falsa la condizione, o la moltiplicazione imminente si sovrapporrà e alleggerirà il compilatore di qualsiasi richiesta di fare o di aver fatto qualcosa. Mentre un simile comportamento può sembrare bizzarro, alcuni autori di compilatori sembrano essere molto orgogliosi delle capacità dei loro compilatori di eliminare tali test "non necessari". Alcune persone potrebbero aspettarsi che un overflow sul secondo multiplo possa, nel peggiore dei casi, causare la corruzione arbitraria di tutti i bit di quel particolare prodotto; infatti, comunque, in ogni caso in cui un compilatore può determinare che l'overflow deve essersi verificato o sarebbe inevitabile prima del successivo effetto secondario osservabile in sequenza, un compilatore sarebbe libero di fare qualsiasi cosa gli piaccia.

    
risposta data 03.11.2014 - 19:30
fonte

Leggi altre domande sui tag