Utilizzo di numeri interi senza segno in C e C ++


23

Ho una domanda molto semplice che mi sconcerta per molto tempo. Ho a che fare con reti e database, quindi molti dati con cui ho a che fare sono contatori a 32 e 64 bit (senza segno), ID di identificazione a 32 e 64 bit (inoltre non hanno una mappatura significativa per il segno). Praticamente non ho mai a che fare con qualsiasi parola vera che possa essere espressa come un numero negativo.

Io e i miei colleghi usiamo abitualmente tipi non firmati come uint32_te uint64_tper queste questioni e, poiché accade così spesso, li usiamo anche per gli indici di array e altri usi interi comuni.

Allo stesso tempo, sto leggendo (ad esempio Google) varie guide alla programmazione che scoraggiano l'uso di tipi interi senza segno e, per quanto ne so, né Java né Scala hanno tipi interi senza segno.

Quindi, non sono riuscito a capire quale sia la cosa giusta da fare: l'uso di valori firmati nel nostro ambiente sarebbe molto scomodo, allo stesso tempo codifica delle guide per insistere nel fare esattamente questo.


Risposte:


31

Ci sono due scuole di pensiero su questo, e nessuno dei due sarà mai d'accordo.

Il primo sostiene che ci sono alcuni concetti intrinsecamente non firmati, come gli indici di array. Non ha senso usare numeri con segno per quelli poiché potrebbe causare errori. Può anche imporre limiti non necessari alle cose: un array che utilizza indici a 32 bit con segno può accedere solo a 2 miliardi di voci, mentre il passaggio a numeri a 32 bit senza segno consente 4 miliardi di voci.

Il secondo sostiene che in qualsiasi programma che utilizza numeri senza segno, prima o poi finirai per fare un'aritmetica mista con segno senza segno. Questo può dare risultati strani e inaspettati: lanciare un grande valore senza segno in segno dà un numero negativo, e viceversa il lancio di un numero negativo in segno senza segno ne dà uno positivo. Questo può essere una grande fonte di errori.


8
Il compilatore rileva problemi aritmetici misti non firmati; mantieni la tua build senza avvisi (con un livello di avviso abbastanza alto). Inoltre, intè più breve da digitare :)
rucamzu,

7
Confessione: sto con la seconda scuola di pensiero, e sebbene comprenda le considerazioni per i tipi senza segno: intè più che sufficiente per gli indici di array il 99,99% delle volte. Le questioni aritmetiche firmate - non firmate sono molto più comuni e hanno quindi la precedenza in termini di cosa evitare. Sì, i compilatori ti avvertono di questo, ma quanti avvisi ricevi quando compili un progetto considerevole? Ignorare gli avvertimenti è pericoloso e cattiva pratica, ma nel mondo reale ...
Elias Van Ootegem,

11
+1 alla risposta. Attenzione : opinioni schiette Ahead : 1: La mia risposta alla seconda scuola di pensiero è: scommetterei che chiunque ottenga risultati inaspettati da tipi integrali non firmati in C avrà un comportamento indefinito (e non di tipo puramente accademico) in i loro programmi C non banali che usano tipi integrali firmati . Se non conosci abbastanza bene C per pensare che i tipi senza segno siano i migliori da usare, ti consiglio di evitare C. 2: Esiste esattamente un tipo corretto per indici e dimensioni di array in C, e questo a size_tmeno che non ci sia un caso speciale buona ragione altrimenti.
mtraceur,

5
Ti trovi nei guai senza firma mista. Calcola solo int senza segno meno int senza segno.
gnasher729,

4
Non metterti in discussione con te, Simon, ma solo con la prima scuola di pensiero che sostiene che "ci sono alcuni concetti intrinsecamente non firmati - come gli indici di array". in particolare: "Esiste esattamente un tipo corretto per gli indici di array ... in C", Cazzate! . Noi DSP utilizziamo sempre indici negativi. in particolare con risposte all'impulso pari o dispari di simmetria che non sono causali. e per LUT matematica. sono nella seconda scuola di pensiero, ma penso che sia utile avere numeri interi sia firmati che non firmati in C e C ++.
robert bristow-johnson,

21

Prima di tutto, la linea guida per la codifica C ++ di Google non è molto buona da seguire: evita cose come eccezioni, boost, ecc. Che sono elementi fondamentali del moderno C ++. In secondo luogo, solo perché una determinata linea guida funziona per l'azienda X non significa che sarà la soluzione giusta per te. Continuerei a usare tipi non firmati, poiché ne hai una buona necessità.

Una regola empirica decente per C ++ è: preferisci a intmeno che tu non abbia una buona ragione per usare qualcos'altro.


8
Non è affatto quello che intendo. I costruttori hanno lo scopo di stabilire gli invarianti e, poiché non sono funzioni, non possono semplicemente return falsestabilire se quell'invariante non è stabilito. Quindi, puoi separare le cose e usare le funzioni init per i tuoi oggetti, oppure puoi lanciare un std::runtime_error, lasciare che si verifichi lo svolgersi dello stack e lasciare che tutti i tuoi oggetti RAII si autopuliscano da soli e lo sviluppatore può gestire l'eccezione dove è conveniente per tu per farlo.
bstamour,

5
Non vedo come il tipo di applicazione faccia la differenza. Ogni volta che chiami un costruttore su un oggetto, stai stabilendo un invariante con i parametri. Se tale invariante non può essere soddisfatto, allora devi segnalare un errore altrimenti il ​​tuo programma non è in buono stato. Poiché i costruttori non possono restituire una bandiera, lanciare un'eccezione è un'opzione naturale. Fornire una valida argomentazione sul motivo per cui un'applicazione aziendale non trarrebbe vantaggio da un tale stile di codifica.
bstamour,

8
Dubito fortemente che metà di tutti i programmatori C ++ non siano in grado di utilizzare correttamente le eccezioni. Ma comunque se pensi che i tuoi colleghi non siano in grado di scrivere C ++ moderno, stai lontano dal C ++ moderno.
bstamour

6
@ zzz777 Non usi le eccezioni? Hanno costruttori privati ​​che sono avvolti da funzioni di fabbrica pubbliche che catturano le eccezioni e fanno cosa: restituiscono un nullptr? restituire un oggetto "predefinito" (qualunque cosa ciò significhi)? Non hai risolto nulla: hai appena nascosto il problema sotto un tappeto e speri che nessuno lo scopra.
Mael,

5
@ zzz777 Se hai intenzione di mandare in crash la scatola comunque, perché ti importa se succede da un'eccezione o signal(6)? Se usi un'eccezione, il 50% degli sviluppatori che sa come gestirli può scrivere un buon codice e il resto può essere trasportato dai loro colleghi.
IllusiveBrian,

6

Le altre risposte mancano di esempi del mondo reale, quindi ne aggiungerò uno. Uno dei motivi per cui (personalmente) cerco di evitare tipi non firmati.

Prendi in considerazione l'utilizzo di size_t standard come indice di array:

for (size_t i = 0; i < n; ++i)
    // do something here;

Ok, perfettamente normale. Quindi, considera che abbiamo deciso di cambiare la direzione del loop per qualche motivo:

for (size_t i = n - 1; i >= 0; --i)
    // do something here;

E ora non funziona. Se intusassimo come iteratore, non ci sarebbero problemi. Ho visto questo errore due volte negli ultimi due anni. Una volta è successo in produzione ed è stato difficile eseguire il debug.

Un altro motivo per me sono gli avvisi fastidiosi, che ti fanno scrivere qualcosa del genere ogni volta :

int n = 123;  // for some reason n is signed
...
for (size_t i = 0; i < size_t(n); ++i)

Queste sono cose minori, ma si sommano. Sento che il codice è più pulito se si usano ovunque numeri interi con segno.

Modifica: certo, gli esempi sembrano stupidi, ma ho visto le persone fare questo errore. Se esiste un modo così semplice per evitarlo, perché non usarlo?

Quando compilo il seguente pezzo di codice con VS2015 o GCC non vedo alcun avviso con le impostazioni di avviso predefinite (anche con -Wall per GCC). Devi chiedere a -Wextra di ricevere un avviso al riguardo in GCC. Questo è uno dei motivi per cui dovresti sempre compilare con Wall e Wextra (e usare l'analizzatore statico), ma in molti progetti di vita reale la gente non lo fa.

#include <vector>
#include <iostream>


void unsignedTest()
{
    std::vector<int> v{ 1, 2 };

    for (int i = v.size() - 1; i >= 0; --i)
        std::cout << v[i] << std::endl;

    for (size_t i = v.size() - 1; i >= 0; --i)
        std::cout << v[i] << std::endl;
}

int main()
{
    unsignedTest();
    return 0;
}

Puoi sbagliarti ancora di più con i tipi firmati ... E il tuo codice di esempio è talmente cerebrale e palesemente sbagliato che qualsiasi compilatore decente avvertirà se chiedi avvisi.
Deduplicatore,

1
In passato ho fatto ricorso a tali orrori da for (size_t i = n - 1; i < n; --i)farlo funzionare bene.
Simon B,

2
Parlando di for-loop con size_tal contrario, esiste una linea guida per la codifica nello stile difor (size_t revind = 0u; revind < n; ++revind) { size_t ind = n - 1u - revind; func(ind); }
rwong,

2
@rwong Omg, è brutto. Perché non usare solo int? :)
Aleksei Petrenko,

1
@AlexeyPetrenko - nota che né gli attuali standard C né C ++ garantiscono che intsia sufficientemente grande da contenere tutti i valori validi di size_t. In particolare, intpuò consentire numeri solo fino a 2 ^ 15-1, e comunemente lo fa su sistemi con limiti di allocazione della memoria di 2 ^ 16 (o in alcuni casi anche superiori). longpuò essere una scommessa più sicura, anche se non è garantito che funzioni. Solo size_tè garantito per funzionare su tutte le piattaforme e in tutti i casi.
Jules il

4
for (size_t i = v.size() - 1; i >= 0; --i)
   std::cout << v[i] << std::endl;

Il problema qui è che hai scritto il ciclo in un modo non ufficiale che porta al comportamento errato. La costruzione del circuito è come i principianti ottengono insegnato per firmati tipi (che è OK e corretta), ma semplicemente non si adatta per i valori senza segno. Ma questo non può servire come contro-argomento contro l'utilizzo di tipi senza segno, il compito qui è semplicemente quello di fare il ciclo giusto. E questo può essere facilmente risolto per funzionare in modo affidabile per tipi senza segno in questo modo:

for (size_t i = v.size(); i-- > 0; )
    std::cout << v[i] << std::endl;

Questa modifica ripristina semplicemente la sequenza del confronto e l'operazione di decremento ed è a mio avviso il modo più efficace, indisturbante, pulito e abbreviato per gestire i contatori senza segno nei cicli a ritroso. Faresti la stessa cosa (intuitivamente) quando usi un ciclo while:

size_t i = v.size();
while (i > 0)
{
    --i;
    std::cout << v[i] << std::endl;
}

Non si può verificare un underflow, il caso di un contenitore vuoto è coperto implicitamente, come nella variante ben nota per il loop del contatore con segno, e il corpo del loop può rimanere inalterato rispetto a un contatore con segno o un loop in avanti. Devi solo abituarti al primo costrutto loop piuttosto strano. Ma dopo aver visto che una dozzina di volte non c'è più nulla di incomprensibile.

Sarei fortunato se i corsi per principianti mostrassero non solo il ciclo corretto per i tipi firmati ma anche quelli non firmati. Ciò eviterebbe un paio di errori che dovrebbero essere incolpati di IMHO per gli sviluppatori inconsapevoli invece di incolpare il tipo senza segno.

HTH


1

Gli interi senza segno sono lì per un motivo.

Si consideri, ad esempio, la consegna dei dati come singoli byte, ad esempio in un pacchetto di rete o in un buffer di file. Occasionalmente potresti imbatterti in bestie come numeri interi a 24 bit. Facilmente spostato da tre numeri interi senza segno a 8 bit, non così semplice con numeri interi con segno a 8 bit.

Oppure pensa agli algoritmi che utilizzano le tabelle di ricerca dei personaggi. Se un carattere è un numero intero senza segno a 8 bit, è possibile indicizzare una tabella di ricerca in base a un valore di carattere. Tuttavia, cosa fai se il linguaggio di programmazione non supporta numeri interi senza segno? Avresti indici negativi su un array. Beh, immagino che potresti usare qualcosa del genere charval + 128ma è semplicemente brutto.

Molti formati di file, infatti, usano numeri interi senza segno e se il linguaggio di programmazione dell'applicazione non supporta numeri interi senza segno, ciò potrebbe costituire un problema.

Quindi prendere in considerazione i numeri di sequenza TCP. Se scrivi qualsiasi codice di elaborazione TCP, vorrai sicuramente utilizzare numeri interi senza segno.

A volte, l'efficienza conta così tanto che hai davvero bisogno di quel pezzo in più di numeri interi senza segno. Considera ad esempio i dispositivi IoT che vengono spediti in milioni. Molte risorse di programmazione possono quindi essere giustificate da spendere per le microottimizzazioni.

Direi che la giustificazione per evitare l'uso di tipi interi senza segno (aritmetica dei segni misti, confronti dei segni misti) può essere superata da un compilatore con opportuni avvertimenti. Tali avvisi di solito non sono abilitati per impostazione predefinita, ma vedi ad esempio -Wextrao separatamente -Wsign-compare(auto-abilitato in C da -Wextra, anche se non penso che sia auto-abilitato in C ++) e -Wsign-conversion.

Tuttavia, in caso di dubbio, utilizzare un tipo firmato. Molte volte, è una scelta che funziona bene. E abilita quegli avvisi del compilatore!


0

Esistono molti casi in cui gli interi non rappresentano effettivamente numeri, ma ad esempio una maschera di bit, un id, ecc. Fondamentalmente casi in cui l'aggiunta di 1 a un numero intero non ha alcun risultato significativo. In questi casi, utilizzare unsigned.

Ci sono molti casi in cui si esegue l'aritmetica con numeri interi. In questi casi, utilizzare numeri interi con segno, per evitare comportamenti errati attorno allo zero. Guarda molti esempi con loop, in cui l'esecuzione di un loop fino a zero utilizza un codice molto poco intuitivo o viene interrotta a causa dell'uso di numeri non firmati. C'è l'argomento "ma gli indici non sono mai negativi" - certo, ma le differenze di indici per esempio sono negative.

Nel caso molto raro in cui gli indici superino 2 ^ 31 ma non 2 ^ 32, non si utilizzano numeri interi senza segno, si utilizzano numeri interi a 64 bit.

Infine, una bella trappola: in un ciclo "for (i = 0; i <n; ++ i) a [i] ..." se sono senza segno a 32 bit e la memoria supera gli indirizzi a 32 bit, il compilatore non può ottimizzare l'accesso a un [i] incrementando un puntatore, perché a i = 2 ^ 32 - 1 mi avvolge. Anche quando n non diventa mai così grande. L'uso di numeri interi con segno evita questo.


-5

Alla fine, ho trovato una risposta davvero buona qui: "Ricettario di programmazione sicura" di J.Viega e M.Messier ( http://shop.oreilly.com/product/9780596003944.do )

Problemi di sicurezza con numeri interi firmati:

  1. Se la funzione richiede un parametro positivo, è facile dimenticare di controllare l'intervallo inferiore.
  2. Schema di bit non intuitivo da conversioni di dimensione intera negativa.
  3. Schema di bit non intuitivo prodotto dall'operazione di spostamento a destra di un numero intero negativo.

Ci sono problemi con le conversioni firmate <-> non firmate, quindi non è consigliabile utilizzare il mix.


1
Perché è una buona risposta? Cos'è la ricetta 3.5? Cosa dice di overflow dei numeri interi ecc.?
Baldrickk,

Nella mia esperienza pratica, è un ottimo libro con preziosi consigli su tutti gli altri aspetti che ho provato ed è piuttosto deciso in questa raccomandazione. Rispetto ai pericoli di overflow di numeri interi su array più lunghi di 4G sembrano piuttosto deboli. Se devo occuparmi di array così grandi, il mio programma avrà un sacco di messa a punto per evitare penalizzazioni delle prestazioni.
zzz777,

1
non si tratta se il libro è buono. La tua risposta non fornisce alcuna giustificazione per l'uso del destinatario e non tutti avranno una copia del libro per cercarlo. Guarda gli esempi di come scrivere una buona risposta
Baldrickk,

FYI ho appena saputo di un'altra ragione per usare numeri interi senza segno: si può facilmente rilevare l'overlow: youtube.com/…
zzz777,
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.