La sottrazione di interi senza segno è definita un comportamento?


100

Mi sono imbattuto nel codice di qualcuno che sembra credere che ci sia un problema durante la sottrazione di un numero intero senza segno da un altro intero dello stesso tipo quando il risultato sarebbe negativo. Quindi quel codice come questo non sarebbe corretto anche se dovesse funzionare sulla maggior parte delle architetture.

unsigned int To, Tf;

To = getcounter();
while (1) {
    Tf = getcounter();
    if ((Tf-To) >= TIME_LIMIT) {
        break;
    } 
}

Questa è l'unica citazione vagamente rilevante dallo standard C che sono riuscito a trovare.

Un calcolo che coinvolge operandi senza segno non può mai superare il flusso, perché un risultato che non può essere rappresentato dal tipo intero senza segno risultante viene ridotto modulo il numero che è maggiore di uno rispetto al valore più grande che può essere rappresentato dal tipo risultante.

Suppongo che si possa prendere quella citazione per significare che quando l'operando destro è più grande l'operazione viene regolata per essere significativa nel contesto dei numeri troncati del modulo.

cioè

0x0000 - 0x0001 == 0x 1 0000 - 0x0001 == 0xFFFF

invece di usare la semantica firmata dipendente dall'implementazione:

0x0000 - 0x0001 == (senza segno) (0 + -1) == (0xFFFF ma anche 0xFFFE o 0x8001)

Quale o quale interpretazione è giusta? È definito del tutto?


3
La scelta delle parole nello standard è sfortunata. Il fatto che "non possa mai traboccare" significa che non è una situazione di errore. Utilizzando la terminologia nello standard, invece di sovraccaricare il valore "wraps".
danorton

Risposte:


107

Il risultato di una sottrazione che genera un numero negativo in un tipo senza segno è ben definito:

  1. [...] Un calcolo che coinvolge operandi senza segno non può mai eccedere, perché un risultato che non può essere rappresentato dal tipo intero senza segno risultante viene ridotto modulo il numero che è maggiore di uno del valore più grande che può essere rappresentato dal tipo risultante. (ISO / IEC 9899: 1999 (E) §6.2.5 / 9)

Come puoi vedere, è (unsigned)0 - (unsigned)1uguale a -1 modulo UINT_MAX + 1, o in altre parole, UINT_MAX.

Nota che anche se dice "Un calcolo che coinvolge operandi non firmati non può mai eccedere", il che potrebbe farti credere che si applica solo per il superamento del limite superiore, questo è presentato come una motivazione per la parte vincolante effettiva della frase: "a il risultato che non può essere rappresentato dal tipo intero senza segno risultante viene ridotto modulo il numero maggiore di uno rispetto al valore più grande che può essere rappresentato dal tipo risultante. " Questa frase non si limita all'overflow del limite superiore del tipo e si applica allo stesso modo ai valori troppo bassi per essere rappresentati.


2
Grazie! Ora vedo l'interpretazione che mi mancava. Penso che avrebbero potuto scegliere una formulazione più chiara però.

4
Mi sento molto meglio ora, sapendo che se un'addizione senza segno arriva a zero e causa caos, sarà perché uintè sempre stata intesa a rappresentare l' anello matematico degli interi 0attraverso UINT_MAX, con le operazioni di addizione e moltiplicazione modulo UINT_MAX+1, e non perché di un trabocco. Tuttavia, solleva la questione del perché, se gli anelli sono un tipo di dati così fondamentale, il linguaggio non offre un supporto più generale per anelli di altre dimensioni.
Theodore Murdock

2
@TheodoreMurdock Penso che la risposta a questa domanda sia semplice. Per quanto ne so, il fatto che sia un anello è una conseguenza, non una causa. Il vero requisito è che i tipi senza segno devono avere tutti i loro bit che partecipano alla rappresentazione del valore. Il comportamento ad anello deriva naturalmente da questo. Se vuoi tale comportamento da altri tipi, allora fai la tua aritmetica seguita dall'applicazione del modulo richiesto; che utilizza operatori fondamentali.
underscore_d

@underscore_d Ovviamente ... è chiaro il motivo per cui hanno preso la decisione di progettazione. È semplicemente divertente che abbiano scritto la specifica all'incirca come "non c'è over / underflow aritmetico perché il tipo di dati è specificato come un anello", come se questa scelta progettuale significasse che i programmatori non devono evitare attentamente il sovra e sotto -fluiscono o fanno fallire i loro programmi in modo spettacolare.
Theodore Murdock

120

Quando si lavora con tipi senza segno , si verifica un'aritmetica modulare (nota anche come comportamento "avvolgente" ). Per capire questa aritmetica modulare , basta dare un'occhiata a questi orologi:

inserisci qui la descrizione dell'immagine

9 + 4 = 1 ( 13 mod 12 ), quindi nell'altra direzione è: 1 - 4 = 9 ( -3 mod 12 ). Lo stesso principio viene applicato quando si lavora con i tipi senza segno. Se il tipo di risultato è unsigned, ha luogo l'aritmetica modulare.


Ora guarda le seguenti operazioni che memorizzano il risultato come unsigned int:

unsigned int five = 5, seven = 7;
unsigned int a = five - seven;      // a = (-2 % 2^32) = 4294967294 

int one = 1, six = 6;
unsigned int b = one - six;         // b = (-5 % 2^32) = 4294967291

Quando vuoi assicurarti che il risultato sia signed, memorizzalo in signedvariabile o eseguirne il cast signed. Quando vuoi ottenere la differenza tra i numeri e assicurarti che l'aritmetica modulare non venga applicata, dovresti considerare l'utilizzo della abs()funzione definita in stdlib.h:

int c = five - seven;       // c = -2
int d = abs(five - seven);  // d =  2

Fai molta attenzione, soprattutto durante la scrittura delle condizioni, perché:

if (abs(five - seven) < seven)  // = if (2 < 7)
    // ...

if (five - seven < -1)          // = if (-2 < -1)
    // ...

if (one - six < 1)              // = if (-5 < 1)
    // ...

if ((int)(five - seven) < 1)    // = if (-2 < 1)
    // ...

ma

if (five - seven < 1)   // = if ((unsigned int)-2 < 1) = if (4294967294 < 1)
    // ...

if (one - six < five)   // = if ((unsigned int)-5 < 5) = if (4294967291 < 5)
    // ...

4
Bello con gli orologi, anche se la prova renderebbe questa la risposta corretta. La premessa della domanda include già l'affermazione che tutto ciò potrebbe essere vero.
Gare di leggerezza in orbita il

5
@LightnessRacesinOrbit: grazie. L'ho scritto perché penso che qualcuno potrebbe trovarlo molto utile. Sono d'accordo, che non è una risposta completa.
LihO

4
La linea int d = abs(five - seven);non va bene. Il primo five - sevenè calcolato: la promozione lascia i tipi di operando come unsigned int, il risultato è calcolato modulo (UINT_MAX+1)e restituisce UINT_MAX-1. Quindi questo valore è il parametro effettivo di abs, che è una cattiva notizia. abs(int)provoca un comportamento indefinito passando l'argomento, poiché non è nell'intervallo e abs(long long)può probabilmente contenere il valore, ma un comportamento indefinito si verifica quando il valore restituito viene costretto inta inizializzare d.
Ben Voigt

1
@LihO: L'unico operatore in C ++ sensibile al contesto e che agisce in modo diverso a seconda di come viene utilizzato il risultato è un operatore di conversione personalizzato operator T(). L'aggiunta nelle due espressioni di cui parliamo viene eseguita in tipo unsigned int, in base ai tipi di operando. Il risultato dell'aggiunta è unsigned int. Quindi quel risultato viene convertito implicitamente nel tipo richiesto nel contesto, una conversione che fallisce perché il valore non è rappresentabile nel nuovo tipo.
Ben Voigt

1
@LihO: Può essere utile pensare a double x = 2/3;vsdouble y = 2.0/3;
Ben Voigt

5

Ebbene, la prima interpretazione è corretta. Tuttavia, il tuo ragionamento sulla "semantica con segno" in questo contesto è sbagliato.

Di nuovo, la tua prima interpretazione è corretta. Follow aritmetica senza segno le regole del modulo aritmetica, il che significa che 0x0000 - 0x0001restituisce 0xFFFFper 32 bit senza segno tipi.

Tuttavia, anche la seconda interpretazione (quella basata sulla "semantica con segno") è necessaria per produrre lo stesso risultato. Vale a dire, anche se si valuta 0 - 1nel dominio di tipo firmato e si ottiene -1come risultato intermedio, questo -1è ancora necessario per produrre 0xFFFFquando successivamente viene convertito in tipo non firmato. Anche se alcune piattaforme utilizzano una rappresentazione esotica per interi con segno (complemento di 1, grandezza con segno), questa piattaforma è ancora richiesta per applicare le regole dell'aritmetica del modulo durante la conversione di valori interi con segno in valori senza segno.

Ad esempio, questa valutazione

signed int a = 0, b = 1;
unsigned int c = a - b;

è ancora garantita per produrre UINT_MAXin c, anche se la piattaforma sta usando una rappresentazione esotica per gli interi con segno.


4
Penso che tu intenda tipi senza segno a 16 bit, non a 32 bit.
xioxox

4

Con numeri di tipo senza segno unsigned into maggiori, in assenza di conversioni di tipo, a-bsi definisce che restituisce il numero senza segno che, se aggiunto b, produrrà a. La conversione di un numero negativo in unsigned è definita come la resa del numero che, quando aggiunto al numero originale con segno invertito, restituirà zero (quindi convertire -5 in unsigned produrrà un valore che, se aggiunto a 5, produrrà zero) .

Nota che i numeri senza segno più piccoli di unsigned intpossono essere promossi al tipo intprima della sottrazione, il comportamento di a-bdipenderà dalla dimensione di int.

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.