Perché gli interi sorprendono
Un intero di macchina ha un numero fisso di bit, quindi qualsiasi operazione può produrre un valore che non ci sta. Cosa succede allora non lo decide l’hardware (che semplicemente gira), ma il linguaggio:
| Linguaggio | Overflow con segno | Overflow senza segno | Scorrimento ≥ larghezza | Divisione per zero |
|---|---|---|---|---|
| C / C++ | comportamento indefinito: l’ottimizzatore assume che non accada mai | gira modulo 2ⁿ (definito) | indefinito | indefinito (SIGFPE su x86) |
| Rust | panic in debug, giro in release; checked_/wrapping_/saturating_/overflowing_ sono espliciti | come con segno | panic in debug | panic |
| Go | gira (definito) | gira | 0 / −1, nessun panic | panic |
| Java | gira (definito); Math.addExact lancia | nessun tipo senza segno (char è unsigned a 16 bit) | contatore mascherato a 5/6 bit | ArithmeticException |
| JavaScript | i numeri sono double: esatti fino a 2⁵³, poi arrotondati; gli operatori sui bit lavorano su int32 | contatore mascherato a 5 bit | Infinity / NaN | |
Le regole del C che mordono
- Promozione degli interi. Tutto ciò che è più piccolo di
intdiventaintprima dell’aritmetica, quindiuint8_t a = 200, b = 100; a + bfa 300, e solo l’assegnazione di ritorno in unuint8_tlo tronca a 44.~xsu unuint8_tdà un int negativo. - Conversioni aritmetiche usuali. Quando un signed incontra un unsigned della stessa larghezza, il lato signed diventa unsigned:
-1 > 0uè vero,if (len - 1 < 0)non è mai vero selenè unsize_t. - L’overflow con segno è UB.
if (x + 1 < x)come controllo di overflow viene eliminato da GCC e Clang con l’ottimizzazione, perché possono assumere chex + 1non vada mai in overflow. Controlla prima dell’operazione (x > INT_MAX - y) o usa__builtin_add_overflow. - Scorrimenti. Scorrere della larghezza o più, o scorrere a sinistra un valore negativo, è indefinito; x86 maschera il contatore, ARM dà zero, il compilatore fa ciò che dice il suo modello.
- Divisione. Tronca verso zero, quindi
-7 / 2 == -3e-7 % 2 == -1;INT_MIN / -1va in overflow e provoca davvero un trap su x86.
Leggere l’output
Il risultato è mostrato nel tipo in cui finisce, con il suo schema di bit e gli stessi bit nella lettura di segno opposta, cioè ciò che vedi quando un valore attraversa un confine (int)/(unsigned) o viene memorizzato attraverso un altro tipo e riletto. La lista dei passaggi è l’ordine di valutazione con il tipo a ogni passo; le note sono i punti in cui il linguaggio ha fatto qualcosa di diverso dalla semplice aritmetica.
Domande
Perché il risultato in C differisce da ciò che ha stampato il mio programma?
Per il comportamento indefinito non c’è un’unica risposta giusta: questa pagina mostra il valore di wraparound che produce il codice non ottimizzato e segna il passaggio come UB. Con -O2 il compilatore può aver assunto che l’overflow non possa accadere e generato altro codice. Compila con -fsanitize=undefined per vedere il punto esatto.
Cosa fa il selettore del tipo?
Imposta il tipo dei letterali senza suffisso né cast, che in C è int di default. Scegli u8 e scrivi 200 + 100 per vedere Rust andare in panic, o C promuovere a int e riuscire.
Come scrivo un cast?
Stile C: (u8) expr, (uint32_t) expr, (int) expr. Stile Rust: expr as u8. Entrambi funzionano in qualsiasi modalità di linguaggio. INT_MAX, INT_MIN, UINT_MAX, LONG_MAX, SIZE_MAX sono disponibili come nomi.
Gestisce i 128 bit?
Sì: i128 e u128 vengono valutati esattamente (tutta l’aritmetica avviene a precisione arbitraria e poi viene adattata al tipo).
E la virgola mobile?
Non qui; per quella c’è il visualizzatore di float IEEE 754 su questo sito. Questo strumento tratta solo ciò che succede ai numeri interi nei tipi a larghezza fissa.