Questo è UB; in termini ISO C ++ l'intero comportamento dell'intero programma è completamente non specificato per un'esecuzione che alla fine colpisce UB. L'esempio classico riguarda lo standard C ++, può far volare i demoni dal naso. (Consiglio di non usare un'implementazione in cui i demoni nasali sono una possibilità reale). Vedi altre risposte per maggiori dettagli.
I compilatori possono "causare problemi" in fase di compilazione per i percorsi di esecuzione che possono vedere portando a un UB visibile in fase di compilazione, ad esempio supponendo che i blocchi di base non vengano mai raggiunti.
Vedi anche quello che ogni programmatore C dovrebbe sapere sul comportamento indefinito (blog LLVM). Come spiegato qui, UB con overflow firmato consente ai compilatori di dimostrare che i for(... i <= n ...)
loop non sono loop infiniti, anche per sconosciuti n
. Inoltre, consente loro di "promuovere" i contatori int loop alla larghezza del puntatore anziché ripetere l'estensione del segno. (Quindi la conseguenza di UB in quel caso potrebbe essere l'accesso al di fuori degli elementi a 64k o 4G bassi di un array, se ti aspettavi un wrapping firmato i
nel suo intervallo di valori.)
In alcuni casi i compilatori emetteranno un'istruzione illegale come x86 ud2
per un blocco che provoca UB in modo dimostrabile se mai eseguito. (Si noti che una funzione potrebbe non essere mai chiamata, quindi i compilatori non possono in generale impazzire e interrompere altre funzioni, o persino possibili percorsi attraverso una funzione che non colpisce UB. Vale a dire che il codice macchina che compila deve ancora funzionare per tutti gli input che non portano a UB.)
Probabilmente la soluzione più efficiente è quella di sbucciare manualmente l'ultima iterazione in modo factor*=10
da evitare l'eventuale necessità .
int result = 0;
int factor = 1;
for (... i < n-1) { // stop 1 iteration early
result = ...
factor *= 10;
}
result = ... // another copy of the loop body, using the last factor
// factor *= 10; // and optimize away this dead operation.
return result;
O se il corpo del loop è grande, considera semplicemente l'uso di un tipo senza segno per factor
. Quindi puoi lasciare che il segno senza segno si moltiplichi e si limiterà a eseguire il wrapping ben definito con una potenza di 2 (il numero di bit di valore nel tipo senza segno).
Questo va bene anche se lo usi con tipi firmati, specialmente se la conversione non firmata> firmata non trabocca mai.
La conversione tra il segno senza segno e il complemento di 2 firmato è gratuita (stesso schema di bit per tutti i valori); il modulo wrapping per int -> unsigned specificato dallo standard C ++ semplifica l'utilizzo dello stesso bit-pattern, a differenza del complemento o del segno / magnitudine.
E unsigned-> signed è altrettanto banale, sebbene sia definito dall'implementazione per valori maggiori di INT_MAX
. Se non stai usando l'enorme risultato non firmato dell'ultima iterazione, non hai nulla di cui preoccuparti. Ma se lo sei, vedi La conversione da unsigned a Sign Undefined? . Il caso value-non-fit è definito dall'implementazione , il che significa che un'implementazione deve scegliere un comportamento; quelli sani semplicemente troncano (se necessario) il pattern di bit senza segno e lo usano come firmato, perché funziona allo stesso modo per i valori nell'intervallo senza alcun lavoro aggiuntivo. E sicuramente non è UB. Quindi i grandi valori senza segno possono diventare numeri interi con segno negativo. ad es. dopo int x = u;
gcc e clang non ottimizzare viax>=0
come sempre vero, anche senza -fwrapv
, perché hanno definito il comportamento.