Lo standard C ++ consente a un bool non inizializzato di arrestare un programma?


500

So che un "comportamento indefinito" in C ++ può praticamente consentire al compilatore di fare tutto ciò che vuole. Tuttavia, ho avuto un incidente che mi ha sorpreso, dato che ho pensato che il codice fosse abbastanza sicuro.

In questo caso, il vero problema si è verificato solo su una piattaforma specifica utilizzando un compilatore specifico e solo se l'ottimizzazione è stata abilitata.

Ho provato diverse cose per riprodurre il problema e semplificarlo al massimo. Ecco un estratto di una funzione chiamata Serialize, che richiederebbe un parametro bool e copia la stringa trueo falsein un buffer di destinazione esistente.

Questa funzione sarebbe in una revisione del codice, non ci sarebbe modo di dire che, in effetti, potrebbe bloccarsi se il parametro bool fosse un valore non inizializzato?

// Zero-filled global buffer of 16 characters
char destBuffer[16];

void Serialize(bool boolValue) {
    // Determine which string to print based on boolValue
    const char* whichString = boolValue ? "true" : "false";

    // Compute the length of the string we selected
    const size_t len = strlen(whichString);

    // Copy string into destination buffer, which is zero-filled (thus already null-terminated)
    memcpy(destBuffer, whichString, len);
}

Se questo codice viene eseguito con l'ottimizzazione di clang 5.0.0 +, potrebbe / potrebbe bloccarsi.

L'aspettato operatore ternario boolValue ? "true" : "false"sembrava abbastanza sicuro per me, stavo assumendo, "Qualunque sia il valore di immondizia boolValuenon ha importanza, dal momento che valuterà comunque vero o falso".

Ho installato un esempio di Explorer compilatore che mostra il problema nello smontaggio, qui l'esempio completo. Nota: per riproporre il problema, la combinazione che ho scoperto ha funzionato utilizzando Clang 5.0.0 con ottimizzazione -O2.

#include <iostream>
#include <cstring>

// Simple struct, with an empty constructor that doesn't initialize anything
struct FStruct {
    bool uninitializedBool;

   __attribute__ ((noinline))  // Note: the constructor must be declared noinline to trigger the problem
   FStruct() {};
};

char destBuffer[16];

// Small utility function that allocates and returns a string "true" or "false" depending on the value of the parameter
void Serialize(bool boolValue) {
    // Determine which string to print depending if 'boolValue' is evaluated as true or false
    const char* whichString = boolValue ? "true" : "false";

    // Compute the length of the string we selected
    size_t len = strlen(whichString);

    memcpy(destBuffer, whichString, len);
}

int main()
{
    // Locally construct an instance of our struct here on the stack. The bool member uninitializedBool is uninitialized.
    FStruct structInstance;

    // Output "true" or "false" to stdout
    Serialize(structInstance.uninitializedBool);
    return 0;
}

Il problema sorge a causa dell'ottimizzatore: è stato abbastanza intelligente dedurre che le stringhe "true" e "false" differiscono solo in lunghezza per 1. Quindi, invece di calcolare davvero la lunghezza, utilizza il valore del bool stesso, che dovrebbe tecnicamente sia 0 o 1, e va così:

const size_t len = strlen(whichString); // original code
const size_t len = 5 - boolValue;       // clang clever optimization

Mentre questo è "intelligente", per così dire, la mia domanda è: lo standard C ++ consente a un compilatore di supporre che un bool possa avere solo una rappresentazione numerica interna di "0" o "1" e usarlo in questo modo?

Oppure questo è un caso definito dall'implementazione, nel qual caso l'implementazione ha presupposto che tutti i suoi bool conterranno sempre 0 o 1, e qualsiasi altro valore è un territorio di comportamento indefinito?


200
È un'ottima domanda È una solida illustrazione di come un comportamento indefinito non sia solo una preoccupazione teorica. Quando la gente dice che tutto ciò può accadere a seguito di UB, quel "qualcosa" può davvero essere abbastanza sorprendente. Si potrebbe presumere che il comportamento indefinito si manifesti ancora in modi prevedibili, ma oggigiorno con moderni ottimizzatori non è affatto vero. OP ha impiegato del tempo per creare un MCVE, ha studiato a fondo il problema, ispezionato lo smontaggio e fatto una domanda chiara e chiara al riguardo. Non potrei chiedere di più.
John Kugelman,

7
Si noti che il requisito secondo il quale "valutazione diversa da zero true" è una regola relativa alle operazioni booleane, tra cui "assegnazione a un bool" (che potrebbe implicitamente invocare un a static_cast<bool>()seconda delle specifiche). Non è tuttavia un requisito per la rappresentazione interna di un boolscelto dal compilatore.
Euro Micelli,

2
I commenti non sono per una discussione estesa; questa conversazione è stata spostata in chat .
Samuel Liew

3
Su una nota molto correlata, questa è una fonte "divertente" di incompatibilità binaria. Se hai un ABI A che azzera i valori prima di chiamare una funzione, ma compila le funzioni in modo tale che presuma che i parametri siano a zero, e un ABI B che è l'opposto (non a zero pad, ma non assume zero con parametri imbottiti), funzionerà principalmente , ma una funzione che utilizza l'ABI B causerà problemi se chiama una funzione che utilizza l'ABI A che accetta un parametro "piccolo". IIRC hai questo su x86 con clang e ICC.
TLW,

1
@TLW: Sebbene lo standard non richieda che le implementazioni forniscano alcun mezzo per chiamare o essere chiamate da codice esterno, sarebbe stato utile disporre di un mezzo per specificare tali elementi per implementazioni laddove rilevanti (implementazioni in cui tali dettagli non lo sono rilevante potrebbe ignorare tali attributi).
supercat

Risposte:


285

Sì, ISO C ++ consente (ma non richiede) implementazioni per fare questa scelta.

Ma nota anche che ISO C ++ consente a un compilatore di emettere codice che si blocca di proposito (ad esempio con un'istruzione illegale) se il programma incontra UB, ad esempio come un modo per aiutarti a trovare errori. (O perché è una DeathStation 9000. Essere rigorosamente conformi non è sufficiente affinché un'implementazione C ++ sia utile per qualsiasi scopo reale). Quindi ISO C ++ consentirebbe a un compilatore di creare un crash che si è bloccato (per motivi totalmente diversi) anche su un codice simile che legge un non inizializzato uint32_t. Anche se è richiesto un tipo di layout fisso senza rappresentazioni trap.

È una domanda interessante su come funzionano le vere implementazioni, ma ricorda che anche se la risposta fosse diversa, il tuo codice sarebbe comunque pericoloso perché il C ++ moderno non è una versione portatile del linguaggio assembly.


Stai compilando per l' ABI System V x86-64 , che specifica che a boolcome una funzione arg in un registro è rappresentato dai bit-pattern false=0etrue=1 negli 8 bit bassi del registro 1 . In memoria, boolè un tipo a 1 byte che deve nuovamente avere un valore intero pari a 0 o 1.

(Un ABI è un insieme di scelte di implementazione su cui i compilatori per la stessa piattaforma concordano in modo che possano creare codice che chiama le reciproche funzioni, tra cui dimensioni dei tipi, regole di layout della struttura e convenzioni di chiamata.)

ISO C ++ non lo specifica, ma questa decisione ABI è diffusa perché rende la conversione bool-> int economica (solo zero-extension) . Non sono a conoscenza di ABI che non consentono al compilatore di assumere 0 o 1 per bool, per qualsiasi architettura (non solo x86). Permette ottimizzazioni come !myboolcon il xor eax,1capovolgere il bit basso: ogni possibile codice che può capovolgere un bit / intero / bool tra 0 e 1 in una singola istruzione CPU . O compilando a&&bun AND bit per bit per i booltipi. Alcuni compilatori sfruttano effettivamente i valori booleani come compilatori a 8 bit. Le operazioni su di essi sono inefficienti? .

In generale, la regola as-if consente al compilatore di sfruttare le cose vere sulla piattaforma di destinazione per la compilazione , poiché il risultato finale sarà un codice eseguibile che implementa lo stesso comportamento visibile esternamente del sorgente C ++. (Con tutte le restrizioni che Undefined Behavior pone su ciò che è effettivamente "esternamente visibile": non con un debugger, ma da un altro thread in un programma C ++ ben formato / legale.)

Il compilatore è sicuramente consentito di trarre pieno vantaggio di una garanzia ABI nel suo codice-gen, e rendere il codice, come hai trovato che ottimizza strlen(whichString)al
5U - boolValue.
(A proposito, questa ottimizzazione è un po 'intelligente, ma forse miope rispetto a branch e inline memcpycome archivi di dati immediati 2. )

Oppure il compilatore avrebbe potuto creare una tabella di puntatori e indicizzarla con il valore intero di bool, supponendo sempre che fosse 0 o 1. ( Questa possibilità è ciò che suggeriva la risposta di @ Barmar .)


Il tuo __attribute((noinline))costruttore con l'ottimizzazione abilitata ha portato a clangare il caricamento di un byte dallo stack da utilizzare come uninitializedBool. Ha fatto spazio per l'oggetto maincon push rax(che è più piccolo e per vari motivi altrettanto efficiente sub rsp, 8), quindi qualunque immondizia fosse in AL all'entrata mainè il valore per cui è stata usata uninitializedBool. Questo è il motivo per cui in realtà hai ottenuto valori che non erano solo 0.

5U - random garbagepuò facilmente passare a un valore non firmato di grandi dimensioni, portando memcpy ad andare nella memoria non mappata. La destinazione è nella memoria statica, non nello stack, quindi non stai sovrascrivendo un indirizzo di ritorno o qualcosa del genere.


Altre implementazioni possono fare scelte diverse, ad esempio, false=0e true=any non-zero value. Quindi probabilmente clang non creerebbe il codice che si arresta in modo anomalo per questa specifica istanza di UB. (Ma sarebbe ancora permesso se lo volesse.) Non conosco implementazioni che scelgono qualcosa di diverso da ciò che fa x86-64 bool, ma lo standard C ++ consente molte cose che nessuno fa o vorrebbe fare hardware simile alle attuali CPU.

ISO C ++ lascia non specificato ciò che troverai quando esamini o modifichi la rappresentazione dell'oggetto di abool . (ad es. memcpying boolin into unsigned char, cosa che puoi fare perché char*può fare qualsiasi alias. Ed unsigned charè garantito che non ha bit di riempimento, quindi lo standard C ++ ti consente formalmente di eseguire il dump delle rappresentazioni degli oggetti senza alcun UB. Puntatore-casting per copiare l'oggetto la rappresentazione è diversa dall'assegnazione char foo = my_bool, ovviamente, quindi la booleanizzazione su 0 o 1 non accadrà e otterresti la rappresentazione dell'oggetto grezzo.)

Hai parzialmente "nascosto" l'UB su questo percorso di esecuzione dal compilatore connoinline . Anche se non è in linea, tuttavia, le ottimizzazioni interprocedurali potrebbero comunque creare una versione della funzione che dipende dalla definizione di un'altra funzione. (In primo luogo, clang sta creando un eseguibile, non una libreria condivisa Unix in cui può avvenire l'interposizione dei simboli. In secondo luogo, la definizione all'interno della class{}definizione, quindi tutte le unità di traduzione devono avere la stessa definizione. Come con la inlineparola chiave.)

Quindi un compilatore potrebbe emettere solo una reto ud2(istruzione illegale) come definizione per main, perché il percorso di esecuzione che inizia in cima mainincontra inevitabilmente un comportamento indefinito. (Che il compilatore può vedere in fase di compilazione se ha deciso di seguire il percorso attraverso il costruttore non in linea.)

Qualsiasi programma che incontra UB è totalmente indefinito per la sua intera esistenza. Ma UB all'interno di una funzione o if()ramo che non viene mai effettivamente eseguito non corrompe il resto del programma. In pratica, ciò significa che i compilatori possono decidere di emettere un'istruzione illegale, o un ret, o di non emettere nulla e cadere nel blocco / funzione successivo, per l'intero blocco di base che può essere provato al momento della compilazione per contenere o portare a UB.

GCC e Clang, in pratica, non in realtà a volte emettono ud2su UB, invece di nemmeno cercare di generare il codice per i percorsi di esecuzione che non hanno senso. O per casi come la fine di una non voidfunzione, gcc a volte omette retun'istruzione. Se stavi pensando che "la mia funzione tornerà con qualunque spazzatura sia in RAX", ti sbagli gravemente. I compilatori C ++ moderni non trattano più il linguaggio come un linguaggio assembly portatile. Il tuo programma deve essere veramente C ++ valido, senza fare ipotesi su come una versione stand-alone non incorporata della tua funzione possa apparire in asm.

Un altro esempio divertente è: Perché l'accesso non allineato alla memoria di mmap a volte segfault su AMD64? . x86 non si guasta su numeri interi non allineati, giusto? Quindi perché un disallineamento uint16_t*sarebbe un problema? Perché alignof(uint16_t) == 2e violare tale presupposto ha portato a un segfault durante la vettorializzazione automatica con SSE2.

Vedi anche Quello che ogni programmatore C dovrebbe sapere sul comportamento indefinito n . 1/3, un articolo di uno sviluppatore di clang.

Punto chiave: se il compilatore ha notato l'UB in fase di compilazione, potrebbe "interrompere" (emettere un asm sorprendente) il percorso attraverso il codice che causa l'UB anche se indirizzato a un ABI in cui qualsiasi bit-pattern è una rappresentazione di oggetto valida per bool.

Aspettati una totale ostilità nei confronti di molti errori da parte del programmatore, in particolare le cose di cui i moderni compilatori avvertono. Questo è il motivo per cui è necessario utilizzare -Walle correggere gli avvisi. Il C ++ non è un linguaggio intuitivo e qualcosa in C ++ può non essere sicuro anche se sarebbe sicuro per quanto riguarda il target per il quale si sta compilando. (ad esempio, l'overflow firmato è UB in C ++ e i compilatori supporranno che non accada, anche durante la compilazione per il complemento x86 di 2, a meno che non lo si usi clang/gcc -fwrapv.)

L'UB visibile in fase di compilazione è sempre pericoloso, ed è davvero difficile essere certi (con l'ottimizzazione del tempo di collegamento) che hai davvero nascosto UB dal compilatore e puoi quindi ragionare sul tipo di asm che genererà.

Non essere troppo drammatico; spesso i compilatori ti permettono di cavartela con alcune cose ed emettere codice come ti aspetti anche quando qualcosa è UB. Ma forse sarà un problema in futuro se gli sviluppatori del compilatore implementano qualche ottimizzazione che ottiene maggiori informazioni sugli intervalli di valori (ad esempio che una variabile non è negativa, forse permettendole di ottimizzare l'estensione del segno per liberare l'estensione zero su x86- 64). Ad esempio, nell'attuale gcc e clang, fare tmp = a+INT_MINnon ottimizza a<0come sempre falso, solo che tmpè sempre negativo. (Perché INT_MIN+ a=INT_MAXè negativo sul target del complemento di questo 2 e anon può essere superiore a quello.)

Quindi gcc / clang al momento non tornano indietro per ricavare informazioni sull'intervallo per gli input di un calcolo, solo sui risultati basati sul presupposto di un overflow non firmato: esempio su Godbolt . Non so se questa ottimizzazione è intenzionalmente "mancata" in nome della facilità d'uso o cosa.

Si noti inoltre che le implementazioni (ovvero i compilatori) possono definire comportamenti che ISO C ++ lascia indefiniti . Ad esempio, tutti i compilatori che supportano i valori intrinseci di Intel (come _mm_add_ps(__m128, __m128)per la vettorializzazione manuale SIMD) devono consentire la formazione di puntatori mal allineati, che è UB in C ++ anche se non li si dereferenziano. __m128i _mm_loadu_si128(const __m128i *)esegue carichi non allineati prendendo un __m128i*argomento non allineato , non un void*o char*. `Reinterpret_cast` tra puntatore vettoriale hardware e tipo corrispondente è un comportamento indefinito?

GNU C / C ++ definisce anche il comportamento dello spostamento a sinistra di un numero con segno negativo (anche senza -fwrapv), separatamente dalle normali regole UB con overflow firmato. ( Questo è UB in ISO C ++ , mentre i turni giusti dei numeri con segno sono definiti dall'implementazione (logica vs. aritmetica); le implementazioni di buona qualità scelgono l'aritmetica su HW che ha turni a destra aritmetici, ma ISO C ++ non specifica). Questo è documentato nella sezione Integer del manuale GCC , insieme alla definizione del comportamento definito dall'implementazione che gli standard C richiedono implementazioni per definire in un modo o nell'altro.

Ci sono sicuramente problemi di qualità dell'implementazione che interessano agli sviluppatori di compilatori; in genere non stanno cercando di creare compilatori intenzionalmente ostili, ma approfittare di tutte le buche UB in C ++ (tranne quelle che scelgono di definire) per ottimizzare meglio può essere quasi indistinguibile a volte.


Nota 1 : i 56 bit superiori possono essere spazzatura che la chiamata deve ignorare, come al solito per tipi più stretti di un registro.

( Altri ABI fanno fare scelte diverse qui . Alcuni non richiedono tipi interi strette per essere zero oppure registrati esteso per riempire un registro quando passato a o restituiti dalle funzioni, come MIPS64 e PowerPC64. Vedere l'ultima sezione di questa risposta x86-64 che confronta rispetto a quei precedenti ISA .)

Ad esempio, un chiamante potrebbe aver calcolato a & 0x01010101in RDI e averlo usato per qualcos'altro, prima di chiamare bool_func(a&1). Il chiamante potrebbe ottimizzare il &1perché lo ha già fatto nel byte basso come parte di and edi, 0x01010101, e sa che è necessario il call per ignorare i byte alti.

O se un bool viene passato come terzo argomento, forse un chiamante che ottimizza per la dimensione del codice lo carica mov dl, [mem]invece di movzx edx, [mem], risparmiando 1 byte al costo di una falsa dipendenza dal vecchio valore di RDX (o altro effetto di registro parziale, a seconda sul modello di CPU). O per il primo argomento, mov dil, byte [r10]invece di movzx edi, byte [r10], perché entrambi richiedono comunque un prefisso REX.

È per questo che emette clang movzx eax, dilin Serialize, invece di sub eax, edi. (Per gli argomenti integer, clang viola questa regola ABI, invece a seconda del comportamento non documentato di gcc e clang a zero stretti o estesi a segno zero o estendi il segno a 32 bit. È necessario un segno o un'estensione zero quando si aggiunge un offset a 32 bit a un puntatore per l'ABI x86-64? Quindi ero interessato a vedere che non fa la stessa cosa per bool.)


Nota 2: dopo la diramazione, avresti solo un movarchivio di 4 byte o un archivio di 4 byte + 1 byte. La lunghezza è implicita nelle larghezze negozio + offset.

OTOH, glibc memcpy eseguirà due carichi / archivi a 4 byte con una sovrapposizione che dipende dalla lunghezza, quindi questo alla fine rende il tutto libero da rami condizionali sul booleano. Vedi il L(between_4_7):blocco in memcpy / memmove di glibc. O almeno, vai allo stesso modo per entrambi i valori booleani nella ramificazione di memcpy per selezionare una dimensione del blocco.

Se in linea, è possibile utilizzare 2x mov-immediate + cmove un offset condizionale, oppure è possibile lasciare in memoria i dati della stringa.

O se l'ottimizzazione per Intel Ice Lake ( con la funzione MOV veloce REP MOV ), un effettivo rep movsbpotrebbe essere ottimale. glibc memcpypotrebbe iniziare a utilizzarlo rep movsb per CPU di piccole dimensioni con quella funzione, risparmiando molte ramificazioni.


Strumenti per rilevare UB e utilizzo di valori non inizializzati

In gcc e clang, è possibile compilare -fsanitize=undefinedper aggiungere la strumentazione di runtime che avviserà o eseguirà un errore su UB che si verifica in fase di esecuzione. Tuttavia, ciò non catturerà variabili unitarie. (Perché non aumenta le dimensioni del tipo per fare spazio a un bit "non inizializzato").

Vedi https://developers.redhat.com/blog/2014/10/16/gcc-undefined-behavior-sanitizer-ubsan/

Per trovare l'utilizzo di dati non inizializzati, c'è un disinfettante per indirizzi e un disinfettante in memoria in clang / LLVM. https://github.com/google/sanitizers/wiki/MemorySanitizer mostra esempi di clang -fsanitize=memory -fPIE -pierilevamento di letture di memoria non inizializzate. Potrebbe funzionare meglio se si compila senza ottimizzazione, quindi tutte le letture delle variabili finiscono per essere effettivamente caricate dalla memoria nell'asm. Mostrano che viene utilizzato -O2in un caso in cui il carico non si ottimizzerebbe. Non l'ho provato da solo. (In alcuni casi, ad esempio non inizializzando un accumulatore prima di sommare un array, clang -O3 emetterà un codice che si somma in un registro vettoriale che non ha mai inizializzato. Quindi, con l'ottimizzazione, è possibile avere un caso in cui non è presente alcuna memoria associata all'UB . Ma-fsanitize=memory cambia l'asm generato e potrebbe comportare un controllo per questo.)

Tollera la copia di memoria non inizializzata e anche semplici operazioni logiche e aritmetiche con essa. In generale, MemorySanitizer tiene traccia silenziosa della diffusione dei dati non inizializzati in memoria e segnala un avviso quando viene acquisito (o non acquisito) un ramo di codice in base a un valore non inizializzato.

MemorySanitizer implementa un sottoinsieme di funzionalità presenti in Valgrind (strumento Memcheck).

Dovrebbe funzionare in questo caso perché la chiamata a glibc memcpycon una lengthmemoria non inizializzata calcolata (all'interno della libreria) si tradurrà in un ramo basato su length. Se avesse incorporato una versione completamente priva di diramazioni che ha appena utilizzato cmov, indicizzato e due negozi, potrebbe non aver funzionato.

Valgrindmemcheck cercherà anche questo tipo di problema, non lamentandosi di nuovo se il programma copia semplicemente dati non inizializzati. Ma dice che rileverà quando un "salto o spostamento condizionato dipende da valori non inizializzati", per cercare di catturare qualsiasi comportamento visibile esternamente che dipende da dati non inizializzati.

Forse l'idea dietro a non contrassegnare solo un carico è che le strutture possono avere il riempimento, e copiare l'intera struttura (incluso il riempimento) con un carico / archivio vettoriale ampio non è un errore anche se i singoli membri sono stati scritti solo uno alla volta. A livello di asm, le informazioni su ciò che era padding e ciò che è effettivamente parte del valore sono state perse.


2
Ho visto un caso peggiore in cui la variabile ha assunto un valore non compreso nell'intervallo di un numero intero a 8 bit, ma solo dell'intero registro della CPU. E Itanium ne ha ancora una peggio, l'uso di una variabile non inizializzata può andare in crash.
Giosuè,

2
@Joshua: oh giusto, buon punto, la speculazione esplicita di Itanium taggerà i valori di registro con un equivalente di "non un numero", in modo tale che usando i difetti di valore.
Peter Cordes,

11
Inoltre, ciò dimostra anche perché il featurebug UB è stato introdotto nella progettazione dei linguaggi C e C ++ in primo luogo: perché fornisce al compilatore esattamente questo tipo di libertà, che ora ha permesso ai compilatori più moderni di eseguire questi di alta qualità ottimizzazioni che rendono C / C ++ linguaggi di medio livello ad alte prestazioni.
The_Sympathizer l'

2
E così continua la guerra tra scrittori di compilatori C ++ e programmatori C ++ che cercano di scrivere programmi utili. Questa risposta, totalmente esaustiva nel rispondere a questa domanda, potrebbe essere utilizzata anche come copia pubblicitaria convincente per i fornitori di strumenti di analisi statica ...
David

4
@The_Sympathizer: UB è stato incluso per consentire alle implementazioni di comportarsi in qualsiasi modo sarebbe più utile per i loro clienti . Non intendeva suggerire che tutti i comportamenti fossero considerati ugualmente utili.
supercat

56

Al compilatore è consentito assumere che un valore booleano passato come argomento sia un valore booleano valido (ovvero uno che è stato inizializzato o convertito in trueo false). Il truevalore non deve essere uguale all'intero 1 - in effetti, possono esserci varie rappresentazioni di truee false- ma il parametro deve essere una rappresentazione valida di uno di questi due valori, dove "rappresentazione valida" è l'implementazione- definito.

Quindi, se non riesci a inizializzare un bool, o se riesci a sovrascriverlo tramite qualche puntatore di un tipo diverso, allora i presupposti del compilatore saranno errati e ne deriverà Undefined Behavior. Sei stato avvertito:

50) L'uso di un valore bool secondo le modalità descritte da questo standard internazionale come "non definito", ad esempio esaminando il valore di un oggetto automatico non inizializzato, potrebbe comportare il comportamento come se non fosse né vero né falso. (Nota in calce al paragrafo 6 del §6.9.1, Tipi fondamentali)


11
Il " truevalore non deve essere uguale all'intero 1" è un po 'fuorviante. Certo, il modello di bit effettivo potrebbe essere qualcos'altro, ma quando convertito / promosso implicitamente (l'unico modo in cui vedresti un valore diverso da true/ false), trueè sempre 1ed falseè sempre0 . Ovviamente, un compilatore del genere non sarebbe in grado di usare il trucco che questo compilatore stava cercando di usare (usando il fatto che boolil modello di bit effettivo poteva essere 0o solo 1), quindi è in qualche modo irrilevante per il problema del PO.
ShadowRanger,

4
@ShadowRanger È sempre possibile ispezionare direttamente la rappresentazione dell'oggetto.
TC

7
@shadowranger: il mio punto è che l'implementazione è responsabile. Se limita rappresentazioni valide trueal modello di bit 1, questa è la sua prerogativa. Se sceglie qualche altra serie di rappresentazioni, in effetti non potrebbe utilizzare l'ottimizzazione indicata qui. Se sceglie quella particolare rappresentazione, allora può. Deve solo essere coerente internamente. È possibile esaminare la rappresentazione di a boolcopiandola in un array di byte; che non è UB (ma è definito dall'implementazione)
rici

3
Sì, l'ottimizzazione dei compilatori (ovvero l'implementazione del C ++ nel mondo reale) a volte emette codice che dipende booldall'avere un modello di bit di 0o 1. Non booleanizzano boologni volta che lo leggono dalla memoria (o un registro che contiene una funzione arg). Ecco cosa dice questa risposta. esempi : gcc4.7 + può ottimizzare return a||ba or eax, ediin una funzione di ritorno bool, o MSVC può ottimizzare a&ba test cl, dl. x86 testè un bit and per bit , quindi se cl=1e dl=2test imposta flag in base a cl&dl = 0.
Peter Cordes,

5
Il punto sul comportamento indefinito è che al compilatore è consentito trarre molte più conclusioni al riguardo, ad esempio supporre che un percorso di codice che porterebbe ad accedere a un valore non inizializzato non sia mai preso affatto, in quanto garantisce che sia esattamente la responsabilità del programmatore . Quindi non si tratta solo della possibilità che i valori di basso livello possano essere diversi da zero o uno.
Holger,

52

La funzione stessa è corretta, ma nel programma di test l'istruzione che chiama la funzione provoca un comportamento indefinito utilizzando il valore di una variabile non inizializzata.

Il bug si trova nella funzione chiamante e potrebbe essere rilevato dalla revisione del codice o dall'analisi statica della funzione chiamante. Usando il link del tuo compilatore Explorer, il compilatore gcc 8.2 rileva il bug. (Forse potresti presentare una segnalazione di bug contro clang che non trova il problema).

Comportamento indefinito significa che può succedere di tutto, incluso il programma che si arresta in modo anomalo di alcune righe dopo l'evento che ha innescato il comportamento indefinito.

NB. La risposta a "Può un comportamento indefinito causare _____?" è sempre "Sì". Questa è letteralmente la definizione di comportamento indefinito.


2
La prima clausola è vera? La semplice copia di un boolUB di trigger non inizializzato ?
Joshua Green

10
@JoshuaGreen see [dcl.init] / 12 "Se un valore indeterminato viene prodotto da una valutazione, il comportamento non è definito tranne nei seguenti casi:" (e nessuno di questi casi ha un'eccezione per bool). La copia richiede una valutazione dell'origine
MM

8
@JoshuaGreen E la ragione di ciò è che potresti avere una piattaforma che innesca un errore hardware se accedi ad alcuni valori non validi per alcuni tipi. Questi sono talvolta chiamati "rappresentazioni di trap".
David Schwartz,

7
Itanium, sebbene oscuro, è una CPU ancora in produzione, ha valori trap e ha almeno due compilatori C ++ semi-moderni (Intel / HP). Ha letteralmente true, falsee not-a-thingvalori per booleani.
Salterio del

3
D'altro canto, la risposta a "Lo standard richiede a tutti i compilatori di elaborare qualcosa in un certo modo" è generalmente "no", anche / soprattutto nei casi in cui è ovvio che qualsiasi compilatore di qualità dovrebbe farlo; più qualcosa di ovvio è, minore sarà la necessità che gli autori dello Standard lo diano effettivamente.
supercat

23

Un bool può contenere solo i valori dipendenti dall'implementazione utilizzati internamente per truee false, e il codice generato può presumere che conterrà solo uno di questi due valori.

In genere, l'implementazione utilizzerà l'intero 0per falsee 1per true, per semplificare le conversioni tra boole inte if (boolvar)generare lo stesso codice di if (intvar). In tal caso, si può immaginare che il codice generato per il ternario nell'assegnazione utilizzerebbe il valore come indice in una matrice di puntatori alle due stringhe, ovvero potrebbe essere convertito in qualcosa del tipo:

// the compile could make asm that "looks" like this, from your source
const static char *strings[] = {"false", "true"};
const char *whichString = strings[boolValue];

Se boolValuenon è inizializzato, potrebbe effettivamente contenere qualsiasi valore intero, il che provocherebbe quindi l'accesso al di fuori dei limiti stringsdell'array.


1
@SidS Grazie. Teoricamente, le rappresentazioni interne potrebbero essere l'opposto di come vengono lanciate verso / da numeri interi, ma ciò sarebbe perverso.
Barmar,

1
Hai ragione, e anche il tuo esempio andrà in crash. Tuttavia è "visibile" a una revisione del codice che si sta utilizzando una variabile non inizializzata come indice di un array. Inoltre, si arresterebbe in modo anomalo anche durante il debug (ad esempio alcuni debugger / compilatori verranno inizializzati con schemi specifici per rendere più semplice la visualizzazione in caso di arresto anomalo). Nel mio esempio, la parte sorprendente è che l'uso del bool è invisibile: l'ottimizzatore ha deciso di usarlo in un calcolo non presente nel codice sorgente.
Remz,

3
@Remz Sto solo usando l'array per mostrare a cosa potrebbe equivalere il codice generato, senza suggerire che qualcuno lo scriverebbe davvero.
Barmar,

1
@Remz Ricompila il boolto intcon *(int *)&boolValuee stampalo per scopi di debug, vedi se è qualcosa di diverso 0o 1quando si blocca. In tal caso, conferma praticamente la teoria secondo cui il compilatore sta ottimizzando l'inline-if come un array che spiega perché si sta arrestando in modo anomalo.
Havenard,

2
@MSalters: std::bitset<8>non mi dà nomi carini per tutte le mie bandiere diverse. A seconda di cosa sono, ciò può essere importante.
Martin Bonner supporta Monica l'

15

Riassumendo molto la tua domanda, ti stai chiedendo Lo standard C ++ consente a un compilatore di supporre che boolpuò avere solo una rappresentazione numerica interna di '0' o '1' e usarla in questo modo?

Lo standard non dice nulla sulla rappresentazione interna di a bool. Definisce solo cosa succede quando si lancia a boolin int(o viceversa). Principalmente, a causa di queste conversioni integrali (e del fatto che le persone si basano piuttosto su di esse), il compilatore utilizzerà 0 e 1, ma non è necessario (anche se deve rispettare i vincoli di qualsiasi ABI di livello inferiore che utilizza ).

Quindi, il compilatore, quando vede un, ha il booldiritto di considerare che detto boolcontiene uno dei bit pattern ' true' o ' false' e fa tutto ciò che sembra. Quindi, se i valori per truee falsesono 1 e 0, rispettivamente, il compilatore è infatti permesso di ottimizzare strlenal 5 - <boolean value>. Sono possibili altri comportamenti divertenti!

Come viene ripetutamente affermato qui, il comportamento indefinito ha risultati indefiniti. Incluso ma non limitato a

  • Il codice funziona come previsto
  • Il tuo codice non riesce in momenti casuali
  • Il tuo codice non viene eseguito affatto.

Guarda cosa dovrebbero sapere tutti i programmatori sul comportamento indefinito

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.