Utilizzo degli interi senza segno in C e C ++

20

Ho una domanda molto semplice che mi sconcerta a lungo. 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), identificativi di identificazione a 32 e 64 bit (inoltre, non hanno una mappatura significativa per il segno). Praticamente non mi occupo mai di alcuna materia reale che possa essere espressa come numero negativo.

Io e i miei collaboratori usiamo di routine tipi non firmati come uint32_t e uint64_t per questi argomenti e poiché accade così spesso li usiamo anche per indici di array e altri usi di interi comuni.

Allo stesso tempo, varie guide di codifica che sto leggendo (ad es. Google) 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: utilizzare i valori firmati nel nostro ambiente sarebbe molto scomodo, allo stesso tempo guide di codifica per insistere nel fare esattamente questo.

    
posta zzz777 29.01.2014 - 17:01
fonte

6 risposte

28

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

Il primo sostiene che ci sono alcuni concetti che sono intrinsecamente non firmati - come gli indici di array. Non ha senso usare numeri firmati per quelli in quanto può portare a errori. Inoltre, può imporre limiti non necessari sulle cose: un array che utilizza indici a 32 bit con segno grafico 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 usa numeri senza segno, prima o poi finirai per fare aritmetica mista con segno senza segno. Ciò può dare risultati strani e inaspettati: il casting di un valore non firmato di grandi dimensioni con un segno assegnato restituisce un numero negativo e, inversamente, un numero negativo a unsigned restituisce un numero positivo elevato. Questa può essere una grande fonte di errori.

    
risposta data 29.01.2014 - 18:08
fonte
20

Prima di tutto, la linea guida per la codifica di Google C ++ non è molto buona da seguire: evita le eccezioni, le eccezioni, ecc. che sono punti fermi del moderno C ++. In secondo luogo, solo perché una certa linea guida funziona per la compagnia X, non significa che sarà la soluzione giusta per te. Continuerò a utilizzare i tipi non firmati, poiché ne hai una buona necessità.

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

    
risposta data 29.01.2014 - 17:20
fonte
6

Le altre risposte mancano di esempi reali, quindi ne aggiungerò una. Uno dei motivi per cui io (personalmente) cerco di evitare tipi non firmati.

Considera l'utilizzo di size_t standard come indice di matrice:

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

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

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

E ora non funziona. Se usassimo int come un 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 fastidiosi avvertimenti, che ti fanno scrivere qualcosa come 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. Mi sembra che il codice sia più pulito se vengono utilizzati solo interi con segno.

Modifica Certo, gli esempi sembrano stupidi, ma ho visto persone fare questo errore. Se c'è 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 -Wextra per avere un avvertimento su questo in GCC. Questo è uno dei motivi per cui dovresti sempre compilare Wall e Wextra (e usare l'analizzatore statico), ma in molti progetti di vita reale le persone non lo fanno.

#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;
}
    
risposta data 13.06.2016 - 13:13
fonte
3
for (size_t i = v.size() - 1; i >= 0; --i)
   std::cout << v[i] << std::endl;

Il problema qui è che hai scritto il loop in modo unclever che porta al comportamento errato. La costruzione del ciclo è come se i principianti lo imparassero per i tipi firmati (che è OK e corretto) ma semplicemente non si adatta ai valori senza segno. Ma questo non può servire come contro-argomento contro l'uso di tipi non firmati, il compito è semplicemente quello di ottenere il ciclo giusto. E questo può facilmente essere corretto per funzionare in modo affidabile per tipi non firmati in questo modo:

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

Questo cambiamento semplicemente ripristina la sequenza del confronto e l'operazione di decremento ed è a mio avviso il modo più efficace, indisturbato, pulito e short per gestire i contatori non firmati nei loop precedenti. 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 può verificarsi alcun underflow, il caso di un contenitore vuoto viene coperto implicitamente, come nella ben nota variante per il ciclo del contatore firmato, e il corpo del loop può rimanere inalterato rispetto a un contatore firmato o ad un ciclo in avanti. Devi solo abituarti a un costrutto di loop inizialmente piuttosto strano. Ma dopo aver visto una dozzina di volte non c'è più niente di incomprensibile.

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

HTH

    
risposta data 21.02.2018 - 11:24
fonte
0

Ci sono molti casi in cui gli interi non rappresentano effettivamente numeri, ma per esempio un bit mask, un id, ecc. Fondamentalmente casi in cui l'aggiunta di 1 a un intero non ha alcun risultato significativo. In questi casi, utilizzare unsigned.

Ci sono molti casi in cui si fa aritmetica con numeri interi. In questi casi, utilizzare numeri interi con segno, per evitare comportamenti scorretti intorno allo zero. Guarda un sacco di esempi con loop, dove l'esecuzione di un ciclo fino a zero utilizza un codice molto poco intuitivo o è rotto a causa dell'uso di numeri senza segno. C'è l'argomento "ma gli indici non sono mai negativi" - certo, ma le differenze degli indici, ad esempio, sono negative.

Nel rarissimo caso in cui gli indici superano 2 ^ 31 ma non 2 ^ 32, non si usano numeri interi senza segno, si usano numeri interi a 64 bit.

Infine, una bella trappola: in un ciclo "per (i = 0; i < n; ++ i) a [i] ..." se io 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é in i = 2 ^ 32 - 1 i si avvolge. Anche quando n non diventa mai così grande. L'utilizzo di interi con segno lo evita.

    
risposta data 05.09.2018 - 23:29
fonte
0

Gli interi senza segno sono lì per un motivo.

Considera, ad esempio, la consegna dei dati come byte individuali, ad es. in un pacchetto di rete o un buffer di file. Occasionalmente potresti incontrare tali bestie come interi a 24 bit. Facile spostamento di bit da tre interi senza segno a 8 bit, non così facile con interi con segno a 8 bit.

Oppure pensa agli algoritmi che usano le tabelle di ricerca dei caratteri. Se un carattere è un numero intero senza segno a 8 bit, è possibile indicizzare una tabella di ricerca per un valore di carattere. Tuttavia, cosa si fa se il linguaggio di programmazione non supporta gli interi senza segno? Avresti indici negativi a un array. Bene, immagino che potresti usare qualcosa come charval + 128 ma è solo brutto.

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

Quindi considera i numeri di sequenza TCP. Se scrivi un codice di elaborazione TCP, ti consigliamo di utilizzare interi senza segno.

A volte, l'efficienza conta così tanto che hai davvero bisogno di quel bit in più di numeri interi senza segno. Si consideri ad esempio i dispositivi IoT che vengono spediti in milioni. Un sacco di risorse di programmazione possono quindi essere giustificate per essere spesi in micro-ottimizzazioni.

Direi che la giustificazione per evitare l'uso di tipi di interi non firmati (aritmetica dei segni misti, confronti di segni misti) può essere superata da un compilatore con avvisi appropriati. Tali avvisi di solito non sono abilitati di default, ma vedi ad es. -Wextra o separatamente -Wsign-compare (abilitato automaticamente in C da -Wextra , anche se non penso sia abilitato automaticamente in C ++) e -Wsign-conversion .

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

    
risposta data 09.09.2018 - 13:04
fonte

Leggi altre domande sui tag