I rami con un comportamento indefinito possono essere considerati irraggiungibili e ottimizzati come codice morto?


89

Considera la seguente dichiarazione:

*((char*)NULL) = 0; //undefined behavior

Invoca chiaramente un comportamento indefinito. L'esistenza di una tale dichiarazione in un dato programma significa che l'intero programma è indefinito o che il comportamento diventa indefinito solo una volta che il flusso di controllo raggiunge questa affermazione?

Il seguente programma sarebbe ben definito nel caso in cui l'utente non inserisca mai il numero 3?

while (true) {
 int num = ReadNumberFromConsole();
 if (num == 3)
  *((char*)NULL) = 0; //undefined behavior
}

O è un comportamento del tutto indefinito, indipendentemente da ciò che l'utente inserisce?

Inoltre, il compilatore può presumere che un comportamento indefinito non verrà mai eseguito in fase di esecuzione? Ciò consentirebbe di ragionare a ritroso nel tempo:

int num = ReadNumberFromConsole();

if (num == 3) {
 PrintToConsole(num);
 *((char*)NULL) = 0; //undefined behavior
}

In questo caso, il compilatore potrebbe pensare che nel caso in cui num == 3invocheremo sempre un comportamento indefinito. Pertanto, questo caso deve essere impossibile e il numero non deve essere stampato. L'intera ifaffermazione potrebbe essere ottimizzata. Questo tipo di ragionamento all'indietro è consentito secondo lo standard?


19
a volte mi chiedo se gli utenti con molti rappresentanti ottengano più voti positivi sulle domande perché "oh hanno molti rappresentanti, questa deve essere una buona domanda" ... ma in questo caso ho letto la domanda e ho pensato "wow, è fantastico "prima ancora che guardassi il richiedente.
turbolenza fino al

4
Penso che il momento in cui emerge il comportamento indefinito, sia indefinito.
eerorika

6
Lo standard C ++ dice esplicitamente che un percorso di esecuzione con un comportamento indefinito in qualsiasi punto è completamente indefinito. Lo interpreterei anche come se dicesse che qualsiasi programma con un comportamento indefinito sul percorso è completamente indefinito (ciò include risultati ragionevoli su altre parti, ma che non è garantito). I compilatori sono liberi di utilizzare il comportamento indefinito per modificare il programma. blog.llvm.org/2011/05/what-every-c-programmer-should-know.html contiene alcuni bei esempi.
Jens

4
@ Jens: Significa davvero solo il percorso di esecuzione. Altrimenti finisci nei guai const int i = 0; if (i) 5/i;.
MSalters

1
Il compilatore in generale non può provare che PrintToConsolenon chiama, std::exitquindi deve effettuare la chiamata.
MSalters

Risposte:


66

L'esistenza di una tale dichiarazione in un dato programma significa che l'intero programma è indefinito o che il comportamento diventa indefinito solo una volta che il flusso di controllo raggiunge questa affermazione?

Nessuno dei due. La prima condizione è troppo forte e la seconda è troppo debole.

Gli accessi agli oggetti a volte sono sequenziati, ma lo standard descrive il comportamento del programma al di fuori del tempo. Danvil ha già citato:

se tale esecuzione contiene un'operazione indefinita, questo standard internazionale non pone alcun requisito sull'implementazione che esegue quel programma con quell'input (nemmeno per quanto riguarda le operazioni che precedono la prima operazione indefinita)

Questo può essere interpretato:

Se l'esecuzione del programma produce un comportamento indefinito, l'intero programma ha un comportamento indefinito.

Quindi, un'istruzione irraggiungibile con UB non dà al programma UB. Una dichiarazione raggiungibile che (a causa dei valori degli input) non viene mai raggiunta, non dà al programma UB. Ecco perché la tua prima condizione è troppo forte.

Ora, il compilatore non può in generale dire cosa ha UB. Quindi, per consentire all'ottimizzatore di riordinare le istruzioni con potenziale UB che sarebbe riordinabile se il loro comportamento fosse definito, è necessario consentire a UB di "tornare indietro nel tempo" e andare storto prima del punto della sequenza precedente (o in C ++ 11 terminologia, affinché l'UB per influenzare le cose che sono sequenziate prima dell'UB). Quindi la tua seconda condizione è troppo debole.

Un importante esempio di ciò è quando l'ottimizzatore si basa su uno stretto alias. Il punto centrale delle rigide regole di aliasing è quello di consentire al compilatore di riordinare operazioni che non potrebbero essere validamente riordinate se fosse possibile che i puntatori in questione alias la stessa memoria. Quindi, se usi puntatori con aliasing illegale e UB si verifica, allora può facilmente influenzare un'istruzione "prima" dell'istruzione UB. Per quanto riguarda la macchina astratta, l'istruzione UB non è stata ancora eseguita. Per quanto riguarda il codice oggetto effettivo, è stato eseguito parzialmente o completamente. Ma lo standard non cerca di entrare nei dettagli su cosa significhi per l'ottimizzatore riordinare le istruzioni, o quali sono le implicazioni di ciò per UB. Dà solo la licenza di implementazione per andare storto non appena lo desidera.

Puoi pensare a questo come "UB ha una macchina del tempo".

Nello specifico per rispondere ai tuoi esempi:

  • Il comportamento è indefinito solo se viene letto 3.
  • I compilatori possono ed eliminano il codice come morto se un blocco di base contiene un'operazione che sicuramente non sarà definita. Sono consentiti (e immagino lo facciano) nei casi che non sono un blocco di base ma in cui tutti i rami portano a UB. Questo esempio non è un candidato a meno che non PrintToConsole(3)sia noto in qualche modo per essere sicuro di tornare. Potrebbe generare un'eccezione o altro.

Un esempio simile al tuo secondo è l'opzione gcc -fdelete-null-pointer-checks, che può accettare codice come questo (non ho controllato questo esempio specifico, consideralo illustrativo dell'idea generale):

void foo(int *p) {
    if (p) *p = 3;
    std::cout << *p << '\n';
}

e modificalo in:

*p = 3;
std::cout << "3\n";

Perché? Perché se pè nullo, il codice ha comunque UB, quindi il compilatore può presumere che non sia nullo e ottimizzare di conseguenza. Il kernel Linux è inciampato su questo ( https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2009-1897 ) essenzialmente perché funziona in una modalità in cui non si suppone che dereferenziare un puntatore nullo essere UB, dovrebbe risultare in un'eccezione hardware definita che il kernel può gestire. Quando l'ottimizzazione è abilitata, gcc richiede l'uso di -fno-delete-null-pointer-checksper fornire quella garanzia oltre gli standard.

PS La risposta pratica alla domanda "quando colpisce un comportamento indefinito?" è "10 minuti prima di partire per la giornata".


4
In realtà, in passato c'erano parecchi problemi di sicurezza a causa di ciò. In particolare, qualsiasi controllo dell'overflow post-fatto rischia di essere ottimizzato a causa di ciò. Ad esempio void can_add(int x) { if (x + 100 < x) complain(); }può essere ottimizzato via del tutto, perché se x+100 doesn' trabocco non succede nulla, e se x+100 fa troppo pieno, che è UB secondo lo standard, quindi nulla potrebbe accadere.
fgp

3
@fgp: giusto, è un'ottimizzazione di cui le persone si lamentano amaramente se ci inciampano, perché inizia a pensare che il compilatore stia deliberatamente violando il tuo codice per punirti. "Perché avrei dovuto scriverlo in quel modo se volevo che lo rimuovessi!" ;-) Ma penso che a volte sia utile all'ottimizzatore quando manipola espressioni aritmetiche più grandi, presumere che non ci sia overflow ed evitare qualcosa di costoso che sarebbe necessario solo in quei casi.
Steve Jessop

2
Sarebbe corretto dire che il programma non è indefinito se l'utente non inserisce mai 3, ma se inserisce 3 durante un'esecuzione l'intera esecuzione diventa indefinita? Non appena è sicuro al 100% che il programma invocherà un comportamento indefinito (e non prima) il comportamento diventa qualsiasi cosa. Queste mie affermazioni sono corrette al 100%?
usr

3
@usr: credo che sia corretto, sì. Con il tuo esempio particolare (e facendo alcune ipotesi sull'inevitabilità dei dati elaborati) penso che un'implementazione potrebbe in linea di principio guardare avanti in STDIN bufferizzato per un 3se lo volesse, e imballare a casa per la giornata non appena ne ha visto uno in arrivo.
Steve Jessop

3
Un +1 extra (se potessi) per il tuo PS
Fred Larson

10

Lo standard indica 1.9 / 4

[Nota: questo standard internazionale non impone requisiti sul comportamento dei programmi che contengono un comportamento indefinito. - nota finale]

Il punto interessante è probabilmente il significato di "contenere". Poco dopo a 1.9 / 5 si afferma:

Tuttavia, se una di tali esecuzioni contiene un'operazione indefinita, la presente norma internazionale 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)

Qui menziona specificamente "l'esecuzione ... con quell'input". Lo interpreterei come un comportamento indefinito in un possibile ramo che non viene eseguito in questo momento non influenza l'attuale ramo di esecuzione.

Un problema diverso tuttavia sono le ipotesi basate su un comportamento non definito durante la generazione del codice. Vedi la risposta di Steve Jessop per maggiori dettagli a riguardo.


1
Se presa alla lettera, questa è la condanna a morte per tutti i programmi reali esistenti.
usr

6
Non penso che la domanda fosse se UB potesse apparire prima che il codice fosse effettivamente raggiunto. La domanda, come ho capito, era se l'UB potesse apparire se il codice non sarebbe stato nemmeno raggiunto. E ovviamente la risposta è "no".
sepp2k

Bene, lo standard non è così chiaro su questo in 1.9 / 4, ma 1.9 / 5 può essere interpretato come quello che hai detto.
Danvil

1
Le note non sono normative. 1.9 / 5 supera la nota in 1.9 / 4
MSalters

5

Un esempio istruttivo è

int foo(int x)
{
    int a;
    if (x)
        return a;
    return 0;
}

Sia l'attuale GCC che l'attuale Clang ottimizzeranno questo (su x86) a

xorl %eax,%eax
ret

perché deducono che xè sempre zero dall'UB nel if (x)percorso di controllo. GCC non ti darà nemmeno un avviso di utilizzo di valore non inizializzato! (perché il passaggio che applica la logica di cui sopra viene eseguito prima del passaggio che genera avvisi di valore non inizializzato)


1
Esempio interessante. È piuttosto brutto che l'abilitazione dell'ottimizzazione nasconda l'avvertimento. Questo non è nemmeno documentato: i documenti di GCC dicono solo che l'abilitazione dell'ottimizzazione produce più avvisi.
sleske

@sleske È brutto, sono d'accordo, ma gli avvisi sui valori non inizializzati sono notoriamente difficili da "correggere": eseguirli perfettamente equivale al problema di arresto, ei programmatori diventano stranamente irrazionali nell'aggiunta di inizializzazioni di variabili "non necessarie" per eliminare i falsi positivi, così gli autori del compilatore finiscono per essere sopra un barile. Ero abituato ad hackerare GCC e ricordo che tutti avevano paura di fare confusione con il passaggio di avvertimento del valore non inizializzato.
Zwol

@zwol: mi chiedo quanto dell '"ottimizzazione" risultante da tale eliminazione di codice morto renda effettivamente più piccolo il codice utile, e quanto finisce per far sì che i programmatori ingrandiscano il codice (ad esempio aggiungendo codice per inizializzare aanche se in tutte le circostanze in cui un non inizializzato averrebbe passato alla funzione che la funzione non farebbe mai nulla con esso)?
supercat

@supercat Non sono stato profondamente coinvolto nel lavoro del compilatore in ~ 10 anni ed è quasi impossibile ragionare sulle ottimizzazioni da esempi di giocattoli. Questo tipo di ottimizzazione tende ad essere associato a riduzioni complessive della dimensione del codice del 2-5% su applicazioni reali, se ricordo bene.
zwol

1
@supercat 2-5% è enorme con queste cose. Ho visto persone sudare per lo 0,1%.
zwol

4

L'attuale bozza di lavoro C ++ dice in 1.9.4 che

La presente norma internazionale non impone requisiti sul comportamento dei programmi che contengono un comportamento indefinito.

Sulla base di ciò, direi che un programma contenente un comportamento indefinito su qualsiasi percorso di esecuzione può fare qualsiasi cosa in ogni momento della sua esecuzione.

Ci sono due ottimi articoli sul comportamento indefinito e su cosa fanno di solito i compilatori:


1
Non ha senso. La funzione int f(int x) { if (x > 0) return 100/x; else return 100; }certamente non richiama mai un comportamento indefinito, anche se 100/0ovviamente è indefinito.
fgp

1
@fgp Quello che dice lo standard (specialmente 1.9 / 5), però, è che se è possibile raggiungere un comportamento indefinito , non importa quando viene raggiunto. Ad esempio, printf("Hello, World"); *((char*)NULL) = 0 non è garantito che stampi nulla. Questo aiuta l'ottimizzazione, perché il compilatore può riordinare liberamente le operazioni (soggette a vincoli di dipendenza, ovviamente) che sa che alla fine si verificheranno, senza dover prendere in considerazione un comportamento indefinito.
fgp

Direi che un programma con la tua funzione non contiene un comportamento indefinito, perché non c'è nessun input dove verrà valutato 100/0.
Jens

1
Esatto, quindi ciò che conta è se l'UB può effettivamente essere attivato o meno, non se può essere attivato teoricamente . O sei pronto a sostenere che int x,y; std::cin >> x >> y; std::cout << (x+y);è consentito dire che "1 + 1 = 17", solo perché ci sono alcuni input in cui x+yoverflow (che è UB poiché intè un tipo con segno).
fgp

Formalmente, direi che il programma ha un comportamento indefinito perché esistono input che lo attivano. Ma hai ragione che questo non ha senso nel contesto del C ++, perché sarebbe impossibile scrivere un programma senza un comportamento indefinito. Mi piacerebbe quando ci fosse un comportamento meno indefinito in C ++, ma non è così che funziona il linguaggio (e ci sono alcune buone ragioni per questo, ma non riguardano il mio utilizzo quotidiano ...).
Jens

3

La parola significa "comportamento" qualcosa è stato fatto . Uno statemenr che non viene mai eseguito non è "comportamento".

Un'illustrazione:

*ptr = 0;

È un comportamento indefinito? Supponiamo di essere certi al 100%ptr == nullptr al almeno una volta durante l'esecuzione del programma. La risposta dovrebbe essere sì.

Che dire di questo?

 if (ptr) *ptr = 0;

È indefinito? (Ricordaptr == nullptr almeno una volta?) Spero proprio di no, altrimenti non sarai in grado di scrivere alcun programma utile.

Nessuno srandardese è stato danneggiato nella realizzazione di questa risposta.


3

Il comportamento indefinito colpisce quando il programma provocherà un comportamento indefinito indipendentemente da ciò che accade dopo. Tuttavia, hai fornito il seguente esempio.

int num = ReadNumberFromConsole();

if (num == 3) {
 PrintToConsole(num);
 *((char*)NULL) = 0; //undefined behavior
}

A meno che il compilatore non conosca la definizione di PrintToConsole, non può rimuovere if (num == 3)condizionale. Supponiamo di avere LongAndCamelCaseStdio.hun'intestazione di sistema con la seguente dichiarazione di PrintToConsole.

void PrintToConsole(int);

Niente di troppo utile, d'accordo. Ora, vediamo quanto malvagio (o forse non così malvagio, un comportamento indefinito avrebbe potuto essere peggiore) è il venditore, controllando l'effettiva definizione di questa funzione.

int printf(const char *, ...);
void exit(int);

void PrintToConsole(int num) {
    printf("%d\n", num);
    exit(0);
}

Il compilatore in realtà deve presumere che qualsiasi funzione arbitraria che il compilatore non sa cosa fa può uscire o generare un'eccezione (nel caso di C ++). Puoi notare che *((char*)NULL) = 0;non verrà eseguito, poiché l'esecuzione non continuerà dopo la PrintToConsolechiamata.

Il comportamento indefinito colpisce quando PrintToConsole effettivamente ritorna. Il compilatore si aspetta che ciò non accada (poiché ciò farebbe sì che il programma esegua un comportamento indefinito qualunque cosa accada), quindi tutto può accadere.

Tuttavia, consideriamo qualcos'altro. Diciamo che stiamo facendo un controllo nullo e usiamo la variabile dopo il controllo nullo.

int putchar(int);

const char *warning;

void lol_null_check(const char *pointer) {
    if (!pointer) {
        warning = "pointer is null";
    }
    putchar(*pointer);
}

In questo caso, è facile notare che lol_null_checkrichiede un puntatore non NULL. L'assegnazione alla warningvariabile globale non volatile non è qualcosa che potrebbe uscire dal programma o generare eccezioni. La pointerè anche non volatile, in modo che non può cambiare magicamente il suo valore nel mezzo della funzione (se lo fa, è un comportamento indefinito). Chiamandolol_null_check(NULL) provocherà un comportamento indefinito che potrebbe causare la non assegnazione della variabile (perché a questo punto, il fatto che il programma esegue il comportamento indefinito è noto).

Tuttavia, il comportamento indefinito significa che il programma può fare qualsiasi cosa. Pertanto, nulla impedisce al comportamento indefinito di tornare indietro nel tempo e di bloccare il programma prima dell'esecuzione della prima riga int main(). È un comportamento indefinito, non deve avere senso. Potrebbe anche andare in crash dopo aver digitato 3, ma il comportamento indefinito tornerà indietro nel tempo e si bloccherà prima ancora di digitare 3. E chissà, forse un comportamento indefinito sovrascriverà la RAM del sistema e causerà il crash del sistema 2 settimane dopo, mentre il tuo programma non definito non è in esecuzione.


Tutti i punti validi. PrintToConsoleè il mio tentativo di inserire un effetto collaterale esterno al programma che è visibile anche dopo i crash ed è fortemente sequenziato. Volevo creare una situazione in cui possiamo dire con certezza se questa affermazione è stata ottimizzata. Ma hai ragione in quanto potrebbe non tornare mai più .; Il tuo esempio di scrittura su un globale potrebbe essere soggetto ad altre ottimizzazioni non correlate a UB. Ad esempio, è possibile eliminare un globale inutilizzato. Hai un'idea per creare un effetto collaterale esterno in modo da garantire il controllo?
usr

Può un qualsiasi effetto collaterale osservabile al di fuori del mondo essere prodotto da codice che un compilatore sarebbe libero di assumere ritorni? Per quanto ne volatileso , anche un metodo che legge semplicemente una variabile potrebbe legittimamente attivare un'operazione di I / O che a sua volta potrebbe interrompere immediatamente il thread corrente; il gestore di interrupt potrebbe quindi terminare il thread prima che abbia la possibilità di eseguire qualsiasi altra cosa. Non vedo alcuna giustificazione con cui il compilatore potrebbe spingere un comportamento indefinito prima di quel punto.
supercat

Dal punto di vista dello standard C, non ci sarebbe nulla di illegale nell'avere un comportamento indefinito perché il computer invii un messaggio ad alcune persone che rintracciano e distruggono tutte le prove delle azioni precedenti del programma, ma se un'azione potrebbe terminare un thread, allora tutto ciò che è sequenziato prima di quell'azione dovrebbe accadere prima di qualsiasi comportamento indefinito che si è verificato dopo di essa.
supercat

1

Se il programma raggiunge un'istruzione che richiama un comportamento indefinito, non viene posto alcun requisito sull'output / comportamento del programma di sorta; non importa se avranno luogo "prima" o "dopo" l'invocazione di un comportamento non definito.

Il tuo ragionamento su tutti e tre gli snippet di codice è corretto. In particolare, un compilatore può trattare qualsiasi istruzione che invoca incondizionatamente un comportamento indefinito nel modo in cui GCC tratta __builtin_unreachable(): come un suggerimento di ottimizzazione che l'istruzione è irraggiungibile (e quindi, che tutti i percorsi di codice che conducono incondizionatamente ad essa sono anch'essi irraggiungibili). Ovviamente sono possibili altre ottimizzazioni simili.


1
Per curiosità, quando hanno __builtin_unreachable()iniziato ad avere effetti che andavano avanti e indietro nel tempo? Dato qualcosa del genere, extern volatile uint32_t RESET_TRIGGER; void RESET(void) { RESET_TRIGGER = 0xAA55; __memorybarrier(); __builtin_unreachable(); }potrei vedere builtin_unreachable()come buono per far sapere al compilatore che può omettere l' returnistruzione, ma sarebbe piuttosto diverso dal dire che il codice precedente potrebbe essere omesso.
supercat

@supercat poiché RESET_TRIGGER è volatile, la scrittura in quella posizione può avere effetti collaterali arbitrari. Per il compilatore è come una chiamata a un metodo opaco. Pertanto, non può essere dimostrato (e non è il caso) che __builtin_unreachablesia raggiunto. Questo programma è definito.
usr

@usr: penserei che i compilatori di basso livello dovrebbero trattare gli accessi volatili come chiamate di metodo opache, ma né clang né gcc lo fanno. Tra le altre cose, una chiamata opaca al metodo potrebbe far sì che tutti i byte di qualsiasi oggetto il cui indirizzo sia stato esposto al mondo esterno e che non sia stato né sarà accessibile da un restrictpuntatore attivo , venga scritto usando un unsigned char*.
supercat

@usr: se un compilatore non tratta un accesso volatile come una chiamata a un metodo opaco per quanto riguarda gli accessi agli oggetti esposti, non vedo alcun motivo particolare per aspettarmi che lo faccia per altri scopi. Lo Standard non richiede che le implementazioni lo facciano, perché ci sono alcune piattaforme hardware in cui un compilatore potrebbe essere in grado di conoscere tutti i possibili effetti di un accesso volatile. Un compilatore adatto per l'uso integrato, tuttavia, dovrebbe riconoscere che gli accessi volatili potrebbero attivare hardware che non era stato inventato quando il compilatore è stato scritto.
supercat

@supercat Penso che tu abbia ragione. Sembra che le operazioni volatili non abbiano "alcun effetto sulla macchina astratta" e quindi non possano terminare il programma o causare effetti collaterali.
usr

1

Molti standard per molti tipi di cose richiedono molti sforzi per descrivere cose che le implementazioni DOVREBBERO o NON DOVREBBERO fare, usando una nomenclatura simile a quella definita in IETF RFC 2119 (sebbene non necessariamente citando le definizioni in quel documento). In molti casi, le descrizioni delle cose che le implementazioni dovrebbero fare tranne nei casi in cui sarebbero inutili o impraticabili sono più importanti dei requisiti a cui devono conformarsi tutte le implementazioni conformi.

Sfortunatamente, gli standard C e C ++ tendono a evitare descrizioni di cose che, sebbene non richieste al 100%, dovrebbero comunque essere attese da implementazioni di qualità che non documentano comportamenti contrari. Un suggerimento che le implementazioni dovrebbero fare qualcosa potrebbe essere visto come implicante che quelle che non lo fanno sono inferiori, e nei casi in cui sarebbe generalmente ovvio quali comportamenti sarebbero utili o pratici, invece che impraticabili e inutili, su una data implementazione, c'era scarsa percezione della necessità che lo Standard interferisca con tali giudizi.

Un compilatore intelligente potrebbe conformarsi allo Standard eliminando qualsiasi codice che non avrebbe alcun effetto tranne quando il codice riceve input che inevitabilmente causerebbero un comportamento indefinito, ma "intelligente" e "stupido" non sono contrari. Il fatto che gli autori dello Standard abbiano deciso che potrebbero esserci alcuni tipi di implementazioni in cui comportarsi in modo utile in una data situazione sarebbe inutile e poco pratico non implica alcun giudizio sul fatto che tali comportamenti debbano essere considerati pratici e utili per gli altri. Se un'implementazione potesse sostenere una garanzia comportamentale senza alcun costo oltre alla perdita di un'opportunità di potatura "ramo morto", quasi qualsiasi valore che il codice utente potrebbe ricevere da tale garanzia supererebbe il costo della sua fornitura. L'eliminazione del ramo morto può andare bene nei casi in cui non sarebbe necessario arrendersi, ma se in una data situazione il codice utente avesse potuto gestire quasi ogni possibile comportamento diverso dall'eliminazione del ramo morto, qualsiasi sforzo che il codice utente avrebbe dovuto spendere per evitare UB sarebbe probabilmente superiore al valore ottenuto da DBE.


È un buon punto che evitare UB possa imporre un costo al codice utente.
usr

@usr: è un punto che i modernisti ignorano completamente. Devo aggiungere un esempio? Ad esempio, se il codice deve valutare x*y < zquando x*ynon va in overflow e in caso di overflow produce 0 o 1 in modo arbitrario ma senza effetti collaterali, non c'è motivo sulla maggior parte delle piattaforme per cui soddisfare il secondo e il terzo requisito dovrebbe essere più costoso di soddisfare il primo, ma qualsiasi modo di scrivere l'espressione per garantire un comportamento definito dallo Standard in tutti i casi in alcuni casi aggiungerebbe un costo significativo. Scrivendo l'espressione come (int64_t)x*y < zpotrebbe più che quadruplicare il costo di calcolo ...
supercat

... su alcune piattaforme, e scrivendolo (int)((unsigned)x*y) < zimpedirebbe a un compilatore di impiegare quelle che altrimenti sarebbero state utili sostituzioni algebriche (es. se lo sa xe zsono uguali e positivi, potrebbe semplificare l'espressione originale a y<0, ma la versione che usa unsigned costringerebbe il compilatore a eseguire la moltiplicazione). Se il compilatore può garantire anche se lo Standard non lo impone, sosterrà il requisito "resa 0 o 1 senza effetti collaterali", il codice utente potrebbe dare al compilatore opportunità di ottimizzazione che altrimenti non potrebbe ottenere.
supercat

Sì, sembra che una forma più lieve di comportamento indefinito sarebbe utile qui. Il programmatore potrebbe attivare una modalità che fa x*yemettere un valore normale in caso di overflow ma qualsiasi valore. UB configurabile in C / C ++ mi sembra importante.
usr

@usr: Se gli autori dello standard C89 fossero stati sinceri nell'affermare che la promozione di valori brevi non firmati per l'accesso è stata la modifica più grave e non erano sciocchi ignoranti, ciò implicherebbe che si aspettavano che nei casi in cui le definivano utili garanzie comportamentali, implementazioni per tali piattaforme avevano reso tali garanzie disponibili ai programmatori, ei programmatori le stavano sfruttando, i compilatori per tali piattaforme avrebbero continuato a offrire tali garanzie comportamentali indipendentemente dal fatto che lo Standard le ordinasse o meno .
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.