Quali sono le migliori pratiche relative agli ints non firmati?


43

Uso ints ovunque non firmati e non sono sicuro che dovrei. Questo può essere dalle colonne ID chiave primaria del database ai contatori, ecc. Se un numero non deve mai essere negativo, userò sempre un int senza segno.

Tuttavia noto dal codice di altri che nessun altro sembra farlo. C'è qualcosa di cruciale che sto trascurando?

Modifica: da questa domanda ho anche notato che in C, la restituzione di valori negativi per errori è all'ordine del giorno piuttosto che generare eccezioni come in C ++.


26
Fai solo attenzione for(unsigned int n = 10; n >= 0; n --)(continua all'infinito)
Chris Burt-Brown,

3
In C e C ++, gli ints senza segno hanno un comportamento di overflow definito con precisione (modulo 2 ^ n). Gli atti firmati non lo fanno. Gli ottimizzatori sfruttano sempre più questo comportamento di overflow indefinito, portando in alcuni casi a risultati sorprendenti.
Steve314

2
Buona domanda! Anch'io una volta sono stato tentato di utilizzare le opzioni per limitare la portata, ma ho scoperto che il rischio / inconveniente superava qualsiasi beneficio / convenienza. La maggior parte delle biblioteche, come hai detto, accetta ints regolari in cui un uint farebbe. Questo rende difficile lavorare con, ma pone anche la domanda: ne vale la pena? In pratica (supponendo che non ti occupi delle cose in modo stupido), raramente avrai un valore di -218 in cui ci si aspetta uno positivo. Che -218 deve essere venuto da qualche parte, giusto? e puoi rintracciarne l'origine. Succede raramente. Utilizza asserzioni, eccezioni, contratti di codice per aiutarti.
Giobbe

@William Ting: se si tratta solo di C / C ++, è necessario aggiungere i tag appropriati alla domanda.
CesarGon,

2
@ Chris: Quanto è significativo il problema del loop infinito nella realtà? Voglio dire, se si fa strada nel rilascio, ovviamente il codice non è stato testato. Anche quando hai bisogno di alcune ore per eseguire il debug la prima volta che commetti questo errore, la seconda volta dovresti sapere cosa cercare prima quando il tuo codice non interrompe il loop.
Sicuro il

Risposte:


28

C'è qualcosa di cruciale che sto trascurando?

Quando i calcoli coinvolgono sia tipi firmati e non firmati che dimensioni diverse, le regole per la promozione dei tipi possono essere complesse e comportare comportamenti imprevisti .

Credo che questo sia il motivo principale per cui Java ha omesso i tipi int senza segno.


3
Un'altra soluzione potrebbe essere quella di richiedere il cast manuale dei numeri come appropriato. Questo è ciò che sembra fare Go (ci ho giocato solo un po 'però) e mi piace più dell'approccio di Java.
Tikhon Jelvis,

2
Questa era una buona ragione per cui Java non includeva il tipo senza segno a 64 bit, e forse un buon motivo per non includere un tipo senza segno a 32 bit [sebbene la semantica dell'aggiunta di valori a 32 bit con segno e senza segno non sarebbe difficile-- tale operazione dovrebbe semplicemente produrre un risultato con segno a 64 bit]. intTuttavia, i tipi non firmati più piccoli di quanto non rappresenterebbero tale difficoltà (dal momento che qualsiasi calcolo promuoverà int); Non ho niente di buono da dire sulla mancanza di un tipo di byte senza segno.
supercat

17

Penso che Michael abbia un punto valido, ma IMO il motivo per cui tutti usano int continuamente (specialmente in for (int i = 0; i < max, i++) è che l'abbiamo imparato in quel modo. Quando ogni singolo esempio in un libro " come imparare a programmare " viene utilizzato intin un forciclo, pochissimi metteranno mai in discussione questa pratica.

L'altra ragione è che intè più corta del 25% rispetto a uint, e siamo tutti pigri ... ;-)


2
Sono d'accordo con il problema educativo. La maggior parte delle persone sembra non mettere mai in discussione ciò che leggono: se è in un libro, non può essere sbagliato, giusto?
Matthieu M.

1
Questo è anche presumibilmente il motivo per cui tutti usano postfix ++durante l'incremento, nonostante il suo particolare comportamento sia raramente necessario e potrebbe anche portare a sfocare inutilmente sulle copie se l'indice del ciclo è un iteratore o un altro tipo non fondamentale (o il compilatore è davvero denso) .
underscore_d

Basta non fare qualcosa del tipo "for (uint i = 10; i> = 0; --i)". L'uso di solo ints per le variabili di loop evita questa possibilità.
David Thornley,


8

Mescolare tipi firmati e non firmati può farti entrare in un mondo di dolore. E non puoi usare tutti i tipi senza segno perché incontrerai cose che hanno un intervallo valido che include numeri negativi o hai bisogno di un valore per indicare un errore e -1 è più naturale. Quindi il risultato netto è che molti programmatori usano tutti i tipi di numeri interi con segno.


1
Forse è una buona pratica non mescolare valori validi con indicazione di errore nella stessa variabile e utilizzare variabili separate per questo. Certo, la libreria standard C non dà un buon esempio qui.
Sicuro il

7

Per me i tipi riguardano molto la comunicazione. Usando esplicitamente un int senza segno mi dici che i valori con segno non sono valori validi. Questo mi consente di aggiungere alcune informazioni durante la lettura del codice oltre al nome della variabile. Idealmente, un tipo non anonimo mi direbbe di più, ma mi dà più informazioni che se avessi usato ints ovunque.

Sfortunatamente non tutti sono molto consapevoli di ciò che comunica il loro codice, e questa è probabilmente la ragione per cui si vede ovunque ovunque anche se i valori sono almeno non firmati.


4
Ma potrei voler limitare i miei valori per un mese solo da 1 a 12. Ne uso un altro tipo? Che ne dici di un mese? Alcune lingue in realtà consentono di limitare valori del genere. Altri, come .Net / C #, forniscono contratti di codice. Certo, numeri interi non negativi si verificano piuttosto frequentemente, ma la maggior parte delle lingue che supportano questo tipo non supportano ulteriori restrizioni. Quindi, si dovrebbe usare una combinazione di uint e controllo degli errori, o semplicemente fare tutto attraverso il controllo degli errori? La maggior parte delle biblioteche non ti chiedono dove abbia senso usarne uno, quindi usarne uno e il casting può essere scomodo.
Giobbe

@Job Direi che dovresti usare una sorta di restrizione forzata per compilatore / interprete sui tuoi mesi. Potrebbe darti un po 'di piastra da installare, ma per il futuro hai una limitazione imposta che impedisce errori e comunica molto più chiaramente quello che ti aspetti. Prevenire gli errori e facilitare la comunicazione è molto più importante dell'inconveniente durante l'implementazione.
daramarak,

1
"Potrei voler limitare i miei valori per un mese solo da 1 a 12" Se hai un set finito di valori come i mesi, dovresti usare un tipo di enumerazione, non numeri interi grezzi.
Josh Caswell,

6

Uso unsigned intin C ++ per gli indici di array, principalmente, e per qualsiasi contatore che inizia da 0. Penso che sia bene dire esplicitamente "questa variabile non può essere negativa".


14
Probabilmente dovresti usare size_t per questo in c ++
JohnB

2
Lo so, non posso proprio disturbarmi.
quant_dev

3

Dovresti preoccuparti di questo quando hai a che fare con un numero intero che potrebbe effettivamente avvicinarsi o superare i limiti di un int firmato. Poiché il massimo positivo di un numero intero a 32 bit è 2.147.483.647, è necessario utilizzare un int senza segno se si sa che a) non sarà mai negativo eb) potrebbe raggiungere 2.147.483.648. Nella maggior parte dei casi, incluse chiavi e contatori del database, non mi accingo mai nemmeno a questo tipo di numeri, quindi non mi preoccupo di preoccuparmi di preoccuparmi se il bit di segno viene utilizzato per un valore numerico o per indicare il segno.

Direi: usa int se non sai di aver bisogno di un int senza segno.


2
Quando si lavora con valori che possono raggiungere i valori massimi, è necessario iniziare a controllare le operazioni per overflow di numeri interi, indipendentemente dal segno. Questi controlli sono generalmente più facili per i tipi senza segno, poiché la maggior parte delle operazioni ha risultati ben definiti senza comportamento indefinito e definito dall'implementazione.
Sicuro il

3

È un compromesso tra semplicità e affidabilità. Più bug possono essere rilevati in fase di compilazione, più affidabile è il software. Persone e organizzazioni diverse si trovano su punti diversi lungo quello spettro.

Se fai mai una programmazione ad alta affidabilità in Ada, usi persino tipi diversi per variabili come la distanza in piedi contro la distanza in metri, e il compilatore lo contrassegna se lo assegni accidentalmente all'altro. È perfetto per programmare un missile guidato, ma è eccessivo (gioco di parole intenzionale) se stai convalidando un modulo web. Non c'è necessariamente nulla di sbagliato in entrambi i casi, purché si adatti ai requisiti.


2

Sono propenso a concordare con il ragionamento di Joel Etherton, ma giungo alla conclusione opposta. Per come la vedo io, anche se sai che è improbabile che i numeri si avvicinino mai ai limiti di un tipo con segno, se sai che i numeri negativi non accadranno, allora ci sono pochissime ragioni per usare la variante firmata di un tipo.

Per lo stesso motivo per cui ho usato, in alcuni casi selezionati BIGINT(intero a 64 bit) anziché INTEGER( intero a 32 bit) nelle tabelle di SQL Server. La probabilità che i dati raggiungano il limite di 32 bit entro un ragionevole lasso di tempo è minima, ma se ciò accade, le conseguenze in alcune situazioni potrebbero essere piuttosto devastanti. Assicurati di mappare correttamente i tipi tra le lingue, o finirai con una stranezza interessante molto in fondo alla strada ...

Detto questo, per alcune cose, come i valori della chiave primaria del database, firmati o non firmati non importa davvero, perché a meno che tu non stia riparando manualmente dati rotti o qualcosa del genere, non hai mai a che fare direttamente con il valore; è un identificatore, niente di più. In questi casi, la coerenza è probabilmente più importante dell'esatta scelta della firma. Altrimenti, si finiscono con alcune colonne di chiavi esterne che sono firmate e altre che non sono firmate, senza alcun modello apparente ad esso - o quella strana interessante di nuovo.


Se stai lavorando con i dati estratti da un sistema SAP, consiglio vivamente BIGINT per i campi ID (come CustomerNumber, ArticleNumber ecc.). Finché nessuno usa stringhe alfanumeriche come ID, cioè ... sigh
Treb

1

Vorrei raccomandare che al di fuori dei contesti di archiviazione e scambio di dati con spazio limitato, si dovrebbero generalmente usare tipi firmati. Nella maggior parte dei casi in cui un numero intero con segno a 32 bit sarebbe troppo piccolo ma un valore senza segno a 32 bit sarebbe sufficiente per oggi, non passerà molto tempo prima che il valore senza segno a 32 bit non sia abbastanza grande.

I tempi principali in cui si dovrebbero usare tipi senza segno sono quando si stanno assemblando più valori in uno più grande (ad es. Convertendo quattro byte in un numero a 32 bit) o ​​decomponendo valori più grandi in quelli più piccoli (ad es. Memorizzando un numero a 32 bit come quattro byte ) o quando si ha una quantità che si prevede che si "capovolga" periodicamente e si deve occuparsene (si pensi a un contatore di servizi residenziali; la maggior parte di loro ha cifre sufficienti per assicurarsi che non possano scorrere tra le letture se vengono letti tre volte all'anno, ma non abbastanza per garantire che non si ribaltino entro la vita utile del misuratore). I tipi senza segno spesso hanno abbastanza "stranezze" che dovrebbero essere usati solo nei casi in cui la loro semantica è necessaria.


1
"Consiglierei [...] generalmente di usare tipi firmati." Hm, hai dimenticato di menzionare i vantaggi dei tipi firmati e hai fornito solo un elenco di quando utilizzare i tipi non firmati. "stranezza" ? Mentre la maggior parte delle operazioni non firmate hanno comportamenti e risultati ben definiti, si immettono comportamenti non definiti e definiti dall'implementazione quando si utilizzano tipi firmati (overflow, bit shift, ...). Hai una strana definizione di "stranezza" qui.
Sicuro il

1
@Secure: la "stranezza" a cui mi riferisco ha a che fare con la semantica degli operatori di confronto, specialmente nelle operazioni che coinvolgono tipi misti firmati e non firmati. È corretto affermare che il comportamento dei tipi con segno non è definito quando si utilizzano valori sufficientemente grandi da traboccare, ma il comportamento dei tipi senza segno può essere sorprendente anche quando si tratta di numeri relativamente piccoli. Ad esempio, (-3) + (1u) è maggiore di -1. Inoltre, alcune normali relazioni matematiche associative che si applicherebbero ai numeri non si applicano ai non firmati. Ad esempio, (ab)> c non implica (ac)> b.
supercat

1
@Secure: Anche se è vero che non si può sempre fare affidamento su tale comportamento associativo con numeri con segno "grande", i comportamenti funzionano come previsto quando si tratta di numeri che sono "piccoli" rispetto al dominio di numeri interi con segno. Al contrario, la non associazione sopra menzionata è problematica con valori non firmati "2 3 1". Per inciso, il fatto che i comportamenti firmati abbiano un comportamento indefinito se utilizzati fuori dai limiti può consentire una migliore generazione di codice su alcune piattaforme quando si utilizzano valori inferiori alla dimensione della parola nativa.
supercat

1
Se questi commenti fossero stati nella tua risposta in primo luogo, invece di una raccomandazione e "insulti" senza fornire alcuna motivazione, non avrei commentato. ;) Anche se non sono ancora d'accordo con "stranezza" qui, è semplicemente la definizione del tipo. Usa lo strumento giusto per il lavoro dato e conosci lo strumento, ovviamente. I tipi senza segno sono lo strumento sbagliato quando hai bisogno di relazioni +/-. C'è un motivo per cui size_tnon è firmato ed ptrdiff_tè firmato.
Sicuro il

1
@Sicuro: se si vuole rappresentare una sequenza di bit, i tipi senza segno sono fantastici; Penso che siamo d'accordo lì. E su alcuni piccoli micro, i tipi senza segno possono essere più efficienti per quantità numeriche. Sono utili anche nei casi in cui i delta rappresentano quantità numeriche ma i valori effettivi no (ad es. Numeri di sequenza TCP). D'altra parte, ogni volta che si sottraggono valori non firmati ci si deve preoccupare dei casi angolari anche quando i numeri sono piccoli; tali matematiche con valori firmati presentano casi angolari solo quando i numeri sono grandi.
supercat

1

Uso ints non firmati per rendere più chiaro il mio codice e le sue intenzioni. Una cosa che faccio per evitare conversioni implicite impreviste quando faccio l'aritmetica con tipi sia firmati che non firmati è usare un short senza segno (2 byte di solito) per le mie variabili senza segno. Questo è efficace per un paio di motivi:

  • Quando si esegue l'aritmetica con variabili e letterali brevi senza segno (che sono di tipo int) o variabili di tipo int, ciò garantisce che la variabile senza segno venga sempre promossa a un int prima di valutare l'espressione, poiché int ha sempre un rango superiore rispetto a breve . Questo evita qualsiasi comportamento imprevisto che fa l'aritmetica con tipi firmati e non firmati, supponendo che il risultato dell'espressione rientri naturalmente in un int firmato.
  • La maggior parte delle volte, le variabili senza segno che stai utilizzando non supereranno il valore massimo di un corto di 2 byte senza segno (65.535)

Il principio generale è che il tipo delle variabili non firmate dovrebbe avere un rango inferiore rispetto al tipo delle variabili firmate al fine di garantire la promozione al tipo firmato. Quindi non avrai alcun comportamento di overflow imprevisto. Ovviamente non puoi assicurarlo tutto il tempo, ma (la maggior parte) spesso è possibile assicurarlo.

Ad esempio, recentemente ne ho avuti alcuni per loop qualcosa del genere:

const unsigned short cuint = 5;
for(unsigned short i=0; i<10; ++i)
{
    if((i-2)%cuint == 0)
    {
       //Do something
    }
}

Il letterale "2" è di tipo int. Se fossi un int senza segno anziché un abbreviato senza segno, nella sottoespressione (i-2), 2 verrebbe promosso a un int senza segno (poiché int senza segno ha una priorità più alta di int con segno). Se i = 0, la sottoespressione è uguale a (0u-2u) = un valore enorme dovuto all'overflow. Stessa idea con i = 1. Tuttavia, poiché i è un abbreviato senza segno, viene promosso allo stesso tipo di "2" letterale, che è firmato int e tutto funziona bene.

Per una maggiore sicurezza: nel raro caso in cui l'architettura su cui si sta implementando causa int come 2 byte, ciò potrebbe far sì che entrambi gli operandi nell'espressione aritmetica vengano promossi in int senza segno nel caso in cui la variabile breve senza segno non si adatti nell'int a 2 byte con segno, quest'ultimo dei quali ha un valore massimo di 32.767 <65.535. (Vedi https://stackoverflow.com/questions/17832815/c-implicit-conversion-signed-unsigned per maggiori dettagli). Per evitare ciò, puoi semplicemente aggiungere un static_assert al tuo programma come segue:

static_assert(sizeof(int) == 4, "int must be 4 bytes");

e non verrà compilato su architetture in cui int è 2 byte.

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.