Cast efficiente da non firmato a firmato che evita comportamenti definiti dall'implementazione


94

Voglio definire una funzione che accetta un unsigned intargomento e restituisce un intmodulo congruente UINT_MAX + 1 all'argomento.

Un primo tentativo potrebbe essere simile a questo:

int unsigned_to_signed(unsigned n)
{
    return static_cast<int>(n);
}

Ma come sa qualsiasi avvocato linguistico, il casting da non firmato a firmato per valori maggiori di INT_MAX è definito dall'implementazione.

Voglio implementarlo in modo tale che (a) si basi solo sul comportamento richiesto dalle specifiche; e (b) si compila in un no-op su qualsiasi macchina moderna e ottimizza il compilatore.

Per quanto riguarda le macchine bizzarre ... Se non c'è un int con segno congruente modulo UINT_MAX + 1 con l'int senza segno, diciamo che voglio lanciare un'eccezione. Se ce n'è più di uno (non sono sicuro che sia possibile), diciamo che voglio quello più grande.

OK, secondo tentativo:

int unsigned_to_signed(unsigned n)
{
    int int_n = static_cast<int>(n);

    if (n == static_cast<unsigned>(int_n))
        return int_n;

    // else do something long and complicated
}

Non mi interessa molto dell'efficienza quando non sono su un tipico sistema a due complementi, poiché a mio modesto parere ciò è improbabile. E se il mio codice diventa un collo di bottiglia sugli onnipresenti sistemi di grandezza dei segni del 2050, beh, scommetto che qualcuno può capirlo e ottimizzarlo.

Ora, questo secondo tentativo è abbastanza vicino a quello che voglio. Sebbene il cast to intsia definito dall'implementazione per alcuni input, il cast back to unsignedè garantito dallo standard per preservare il valore modulo UINT_MAX + 1. Quindi il condizionale controlla esattamente quello che voglio e non si compilerà in nulla su nessun sistema che potrei incontrare.

Tuttavia ... sto ancora trasmettendo intsenza prima controllare se richiamerà il comportamento definito dall'implementazione. Su qualche ipotetico sistema nel 2050 potrebbe fare chissà cosa. Quindi diciamo che voglio evitarlo.

Domanda: come dovrebbe essere il mio "terzo tentativo"?

Per ricapitolare, voglio:

  • Trasmetti da int non firmato a int firmato
  • Conserva il valore mod UINT_MAX + 1
  • Invoca solo comportamenti obbligatori standard
  • Compilare in modo non operativo su una tipica macchina a complemento a due con ottimizzazione del compilatore

[Aggiornare]

Faccio un esempio per mostrare perché questa non è una domanda banale.

Considera un'ipotetica implementazione C ++ con le seguenti proprietà:

  • sizeof(int) è uguale a 4
  • sizeof(unsigned) è uguale a 4
  • INT_MAX è uguale a 32767
  • INT_MINè uguale a -2 32 + 32768
  • UINT_MAXè uguale a 2 32 - 1
  • L'aritmetica su intè modulo 2 32 (nell'intervallo INT_MINfino a INT_MAX)
  • std::numeric_limits<int>::is_modulo è vero
  • Il cast di unsigned na int conserva il valore per 0 <= n <= 32767 e restituisce zero in caso contrario

In questa ipotetica implementazione, esiste esattamente un intvalore congruente (mod UINT_MAX + 1) a ciascun unsignedvalore. Quindi la mia domanda sarebbe ben definita.

Affermo che questa ipotetica implementazione C ++ è completamente conforme alle specifiche C ++ 98, C ++ 03 e C ++ 11. Ammetto di non aver memorizzato ogni parola di tutti loro ... Ma credo di aver letto attentamente le sezioni pertinenti. Quindi, se vuoi che accetti la tua risposta, devi (a) citare una specifica che esclude questa ipotetica implementazione o (b) gestirla correttamente.

Una risposta corretta, infatti, deve gestire ogni ipotetica implementazione consentita dalla norma. Questo è ciò che significa "invocare solo comportamenti obbligatori standard", per definizione.

Per inciso, nota che std::numeric_limits<int>::is_moduloè assolutamente inutile qui per molteplici ragioni. Per prima cosa, può essere trueanche se i cast da non firmato a firmato non funzionano per valori grandi senza segno. Per un altro, può essere trueanche su sistemi di complemento a uno o di grandezza dei segni, se l'aritmetica è semplicemente modulo dell'intero intervallo di numeri interi. E così via. Se la tua risposta dipende is_modulo, è sbagliata.

[Aggiorna 2]

La risposta di hvd mi ha insegnato qualcosa: la mia ipotetica implementazione C ++ per interi non è consentita dal C. moderno. Gli standard C99 e C11 sono molto specifici sulla rappresentazione di interi con segno; in effetti, consentono solo il complemento a due, il complemento a uno e la grandezza del segno (sezione 6.2.6.2 paragrafo (2);).

Ma C ++ non è C. A quanto pare, questo fatto è al centro della mia domanda.

Lo standard C ++ 98 originale era basato sul C89 molto più vecchio, che dice (sezione 3.1.2.5):

Per ciascuno dei tipi di interi con segno, esiste un tipo di intero senza segno corrispondente (ma diverso) (designato con la parola chiave unsigned) che utilizza la stessa quantità di memoria (comprese le informazioni sul segno) e ha gli stessi requisiti di allineamento. L'intervallo di valori non negativi di un tipo intero con segno è un sottointervallo del corrispondente tipo intero senza segno e la rappresentazione dello stesso valore in ogni tipo è la stessa.

C89 non dice nulla sull'avere solo un bit di segno o consentire solo complementi a due / complementi a uno / magnitudine del segno.

Lo standard C ++ 98 ha adottato questo linguaggio quasi letteralmente (sezione 3.9.1 paragrafo (3)):

Per ciascuno dei tipi di interi con segno , esiste un tipo di intero senza segno corrispondente (ma diverso) : " unsigned char", " unsigned short int", " unsigned int" e " unsigned long int", ognuno dei quali occupa la stessa quantità di memoria e ha gli stessi requisiti di allineamento (3.9 ) come tipo intero con segno corrispondente; cioè, ogni intero con segno tipo ha la stessa rappresentazione dell'oggetto come corrispondente intero senza segno tipo. L'intervallo di valori non negativi di un tipo intero con segno è un sottointervallo del corrispondente tipo intero senza segno e la rappresentazione del valore di ciascun tipo con segno / senza segno corrispondente deve essere la stessa.

Lo standard C ++ 03 utilizza un linguaggio essenzialmente identico, così come C ++ 11.

Nessuna specifica C ++ standard vincola le sue rappresentazioni di interi con segno a qualsiasi specifica C, per quanto ne so. E non c'è niente che imponga un singolo segno o qualcosa del genere. Tutto ciò che dice è che gli interi con segno non negativo devono essere un sottointervallo del corrispondente senza segno.

Quindi, ancora una volta sostengo che INT_MAX = 32767 con INT_MIN = -2 32 +32768 è consentito. Se la tua risposta presume il contrario, non è corretta a meno che tu non citi uno standard C ++ che mi dimostra che ho torto.


@SteveJessop: In realtà, ho dichiarato esattamente quello che voglio in quel caso: "Se non c'è un modulo int congruente UINT_MAX + 1 con segno int non firmato, diciamo che voglio lanciare un'eccezione." Cioè, voglio il "right" firmato int purché esista. Se non esiste, come potrebbe accadere, ad esempio, nel caso di bit di riempimento o rappresentazioni di complemento a uno, voglio rilevarlo e gestirlo per quella particolare invocazione del cast.
Nemo

scusa, non sono sicuro di come mi sia perso.
Steve Jessop

A proposito, penso che nella tua ipotetica implementazione complicata siano intnecessari almeno 33 bit per rappresentarla. So che è solo una nota a piè di pagina, quindi puoi sostenere che non è normativa, ma penso che la nota 49 in C ++ 11 sia intesa come vera (poiché è una definizione di un termine usato nello standard) e non contraddice nulla di esplicitamente dichiarato nel testo normativo. Quindi tutti i valori negativi devono essere rappresentati da uno schema di bit in cui è impostato il bit più alto, e quindi non è possibile raggrupparli 2^32 - 32768in 32 bit. Non che la tua argomentazione si basi in alcun modo sulla dimensione di int.
Steve Jessop

E per quanto riguarda le tue modifiche nella risposta di hvd, penso che tu abbia interpretato male la nota 49. Dici che la grandezza dei segni è proibita, ma non lo è. Lo avete letto come: "i valori rappresentati dai bit successivi sono additivi, iniziano con 1, e (sono moltiplicati per la potenza integrale successiva di 2, tranne forse per il bit con la posizione più alta)". Credo si debba leggere "i valori rappresentati da bit successivi (sono additivi, iniziano con 1, e sono moltiplicati per potenza integrale successiva di 2), tranne forse per il bit con la posizione più alta". Cioè, tutte le scommesse sono disattivate se è impostato il bit alto.
Steve Jessop

@SteveJessop: la tua interpretazione potrebbe essere corretta. Se è così, esclude il mio ipotetico ... Ma introduce anche un numero veramente vasto di possibilità, rendendo questa domanda estremamente difficile da rispondere. Questo in realtà mi sembra un bug nelle specifiche. (Apparentemente, il comitato C lo pensava e lo ha risolto completamente in C99. Mi chiedo perché C ++ 11 non abbia adottato il loro approccio?)
Nemo

Risposte:


70

Espandendo la risposta di user71404:

int f(unsigned x)
{
    if (x <= INT_MAX)
        return static_cast<int>(x);

    if (x >= INT_MIN)
        return static_cast<int>(x - INT_MIN) + INT_MIN;

    throw x; // Or whatever else you like
}

Se x >= INT_MIN(tieni a mente le regole della promozione, INT_MINviene convertito in unsigned), quindi x - INT_MIN <= INT_MAX, quindi non avrà alcun overflow.

Se ciò non è ovvio, dai un'occhiata all'affermazione "Se x >= -4u, allora x + 4 <= 3." E tieni presente che INT_MAXsarà uguale almeno al valore matematico di -INT_MIN - 1.

Sui sistemi più comuni, dove !(x <= INT_MAX)implica x >= INT_MIN, l'ottimizzatore dovrebbe essere in grado (e sul mio sistema, è in grado) di rimuovere il secondo controllo, determinare che le due returnistruzioni possono essere compilate con lo stesso codice e rimuovere anche il primo controllo. Elenco assemblaggio generato:

__Z1fj:
LFB6:
    .cfi_startproc
    movl    4(%esp), %eax
    ret
    .cfi_endproc

L'implementazione ipotetica nella tua domanda:

  • INT_MAX è uguale a 32767
  • INT_MIN è uguale a -2 32 + 32768

non è possibile, quindi non necessita di particolari considerazioni. INT_MINsarà uguale a -INT_MAXo a -INT_MAX - 1. Ciò deriva dalla rappresentazione in C dei tipi interi (6.2.6.2), che richiede che i nbit siano bit di valore, un bit sia un bit di segno e consente solo una singola rappresentazione trap (escluse le rappresentazioni non valide a causa di bit di riempimento), vale a dire quello che altrimenti rappresenterebbe zero / negativo -INT_MAX - 1. Il C ++ non consente alcuna rappresentazione di interi oltre a ciò che C consente.

Aggiornamento : il compilatore di Microsoft apparentemente non se ne accorgex > 10ex >= 11prova la stessa cosa. Genera solo il codice desiderato sex >= INT_MINviene sostituito conx > INT_MIN - 1u, che può rilevare come la negazione dix <= INT_MAX(su questa piattaforma).

[Aggiornamento dall'interrogante (Nemo), elaborando la nostra discussione di seguito]

Ora credo che questa risposta funzioni in tutti i casi, ma per ragioni complicate. È probabile che assegnerò la taglia a questa soluzione, ma voglio catturare tutti i dettagli cruenti nel caso in cui qualcuno si preoccupi.

Cominciamo con C ++ 11, sezione 18.3.3:

La Tabella 31 descrive l'intestazione <climits>.

...

I contenuti sono gli stessi dell'intestazione della libreria C standard <limits.h>.

Qui, "Standard C" significa C99, la cui specifica limita fortemente la rappresentazione di interi con segno. Sono proprio come interi senza segno, ma con un bit dedicato al "segno" e zero o più bit dedicati al "riempimento". I bit di riempimento non contribuiscono al valore dell'intero e il bit di segno contribuisce solo come complemento a due, complemento a uno o grandezza del segno.

Poiché C ++ 11 eredita le <climits>macro da C99, INT_MIN è -INT_MAX o -INT_MAX-1, e il codice di hvd è garantito per funzionare. (Nota che, a causa del riempimento, INT_MAX potrebbe essere molto inferiore a UINT_MAX / 2 ... Ma grazie al modo in cui funzionano i cast con segno-> non firmato, questa risposta gestisce bene.)

C ++ 03 / C ++ 98 è più complicato. Utilizza la stessa dicitura per ereditare <climits>da "Standard C", ma ora "Standard C" significa C89 / C90.

Tutti questi - C ++ 98, C ++ 03, C89 / C90 - hanno la formulazione che fornisco nella mia domanda, ma includono anche questa (C ++ 03 sezione 3.9.1 paragrafo 7):

Le rappresentazioni dei tipi integrali definiscono i valori utilizzando un sistema di numerazione binario puro. (44) [ Esempio : la presente norma internazionale consente rappresentazioni con complemento a 2, complemento a 1 e magnitudo con segno per i tipi integrali.]

La nota (44) definisce "sistema di numerazione binaria pura":

Una rappresentazione di posizione per interi che utilizza le cifre binarie 0 e 1, in cui i valori rappresentati dai bit successivi sono additivi, iniziano con 1 e vengono moltiplicati per la potenza integrale successiva di 2, tranne forse per il bit con la posizione più alta.

Ciò che è interessante di questa formulazione è che contraddice se stessa, perché la definizione di "sistema di numerazione binaria pura" non consente una rappresentazione segno / grandezza! Consente al bit alto di avere, ad esempio, il valore -2 n-1 (complemento a due) o - (2 n-1 -1) (complemento a unità). Ma non esiste alcun valore per il bit alto che si traduce in segno / grandezza.

Ad ogni modo, la mia "implementazione ipotetica" non si qualifica come "binaria pura" in questa definizione, quindi è esclusa.

Tuttavia, il fatto che il bit alto sia speciale significa che possiamo immaginare che contribuisca a qualsiasi valore: un piccolo valore positivo, un enorme valore positivo, un piccolo valore negativo o un enorme valore negativo. (Se il bit di segno può contribuire - (2 n-1 -1), perché no - (2 n-1 -2)? Ecc.)

Quindi, immaginiamo una rappresentazione di un intero con segno che assegni un valore stravagante al bit "segno".

Un piccolo valore positivo per il bit di segno risulterebbe in un intervallo positivo per int(possibilmente grande quanto unsigned), e il codice hvd lo gestisce perfettamente.

Un enorme valore positivo per il bit di segno risulterebbe in intun massimo maggiore di unsigned, che è vietato.

Un enorme valore negativo per il bit di segno comporterebbe la intrappresentazione di un intervallo di valori non contiguo e altre diciture nelle specifiche lo escludono.

Infine, che ne dici di un bit di segno che contribuisce con una piccola quantità negativa? Potremmo avere un 1 nel "bit di segno" che contribuisce, diciamo, -37 al valore di int? Quindi INT_MAX sarebbe (diciamo) 2 31 -1 e INT_MIN sarebbe -37?

Ciò comporterebbe che alcuni numeri abbiano due rappresentazioni ... Ma il complemento a uno dà due rappresentazioni a zero, e ciò è consentito secondo l '"Esempio". Da nessuna parte le specifiche dicono che zero è l' unico numero intero che potrebbe avere due rappresentazioni. Quindi penso che questa nuova ipotesi sia consentita dalle specifiche.

In effetti, qualsiasi valore negativo da -1 fino a -INT_MAX-1sembra essere ammissibile come valore per il "bit di segno", ma niente di più piccolo (per evitare che l'intervallo sia non contiguo). In altre parole, INT_MINpotrebbe essere qualsiasi cosa da -INT_MAX-1a -1.

Ora, indovina cosa? Per il secondo cast del codice hvd per evitare comportamenti definiti dall'implementazione, abbiamo solo bisogno di x - (unsigned)INT_MINminore o uguale a INT_MAX. Abbiamo appena mostrato INT_MINalmeno -INT_MAX-1. Ovviamente lo xè al massimo UINT_MAX. Trasmettere un numero negativo a unsigned equivale ad aggiungere UINT_MAX+1. Metterli tutti insieme:

x - (unsigned)INT_MIN <= INT_MAX

se e solo se

UINT_MAX - (INT_MIN + UINT_MAX + 1) <= INT_MAX
-INT_MIN-1 <= INT_MAX
-INT_MIN <= INT_MAX+1
INT_MIN >= -INT_MAX-1

Quest'ultimo è quello che abbiamo appena mostrato, quindi anche in questo caso perverso, il codice funziona davvero.

Ciò esaurisce tutte le possibilità, ponendo così fine a questo esercizio estremamente accademico.

Conclusione: in C89 / C90 esiste un comportamento seriamente sotto-specificato per gli interi con segno che sono stati ereditati da C ++ 98 / C ++ 03. È stato risolto in C99 e C ++ 11 eredita indirettamente la correzione incorporandola <limits.h>da C99. Ma anche C ++ 11 mantiene la dicitura contraddittoria "rappresentazione binaria pura" ...


Domanda aggiornata. Sto votando per difetto questa risposta (per ora) per scoraggiare gli altri ... Annullerò il voto più tardi perché la risposta è interessante. (Corretto per C, ma sbagliato per C ++. Penso.)
Nemo

@Nemo Lo standard C si applica al C ++ in questo caso; come minimo, i valori in <limits.h>sono definiti nello standard C ++ come aventi lo stesso significato dello standard C, quindi tutti i requisiti di C per INT_MINe INT_MAXsono ereditati in C ++. Hai ragione che C ++ 03 si riferisce a C90 e C90 è vago sulle rappresentazioni di interi consentite, ma la modifica di C99 (ereditata almeno tramite <limits.h>C ++ 11, si spera anche in modo più diretto) per limitarla a quei tre erano uno che codificava la pratica esistente: non esistevano altre implementazioni.

Sono d'accordo che il significato di INT_MINecc. Sia ereditato da C. Ma ciò non significa che i valori lo siano. (In effetti, come potrebbero, dal momento che ogni implementazione è diversa?) La tua inferenza che INT_MINè entro 1 -INT_MAXdipende dalla formulazione che semplicemente non appare in nessuna specifica C ++. Quindi, mentre C ++ eredita il significato semantico delle macro, la specifica non fornisce (o eredita) la formulazione che supporta la tua inferenza. Questa sembra essere una svista nella specifica C ++ che impedisce un cast efficiente e completamente conforme da non firmato a firmato.
Nemo

@Nemo Se affermi (forse correttamente) che il C ++ consente altre rappresentazioni, allora su tale implementazione, sostengo che INT_MIN non è necessario che sia il valore rappresentabile minimo di tipo int, perché per quanto riguarda C, se il tipo non lo fa soddisfare i requisiti di int, lo standard C non può coprire tale implementazione in alcun modo, e lo standard C ++ non ne fornisce alcuna definizione diversa da "ciò che dice lo standard C". Controllerò se c'è una spiegazione più semplice.

7
Questo è stupendo. Non ho idea di come mi sia sfuggita questa domanda in quel momento.
Gare di leggerezza in orbita

17

Questo codice si basa solo sul comportamento, imposto dalle specifiche, quindi il requisito (a) è facilmente soddisfatto:

int unsigned_to_signed(unsigned n)
{
  int result = INT_MAX;

  if (n > INT_MAX && n < INT_MIN)
    throw runtime_error("no signed int for this number");

  for (unsigned i = INT_MAX; i != n; --i)
    --result;

  return result;
}

Non è così facile con il requisito (b). Questo compila in un no-op con gcc 4.6.3 (-Os, -O2, -O3) e con clang 3.0 (-Os, -O, -O2, -O3). Intel 12.1.0 si rifiuta di ottimizzare questo. E non ho informazioni su Visual C.


1
OK, è fantastico. Vorrei poter dividere la taglia 80:20 ... Ho il sospetto che il ragionamento del compilatore sia: se il ciclo non termina, va in resultoverflow; l'overflow dell'intero non è definito; quindi il ciclo termina; quindi i == nalla risoluzione; quindi resultuguale n. Devo ancora preferire la risposta di hvd (per il comportamento non patologico su compilatori meno intelligenti), ma questo merita più voti positivi.
Nemo

1
Unsigned sono definiti come modulo. Il ciclo è garantito anche per terminare perché nè un valore senza segno e ialla fine deve raggiungere ogni valore senza segno.
idupree

7

La risposta originale ha risolto il problema solo per unsigned=> int. E se volessimo risolvere il problema generale di "qualche tipo senza segno" nel suo tipo con segno corrispondente? Inoltre, la risposta originale era eccellente nel citare sezioni dello standard e analizzare alcuni casi d'angolo, ma non mi ha davvero aiutato a farmi un'idea del perché funzionasse, quindi questa risposta cercherà di fornire una solida base concettuale. Questa risposta cercherà di spiegare il "perché" e di utilizzare le moderne funzionalità C ++ per cercare di semplificare il codice.

Risposta in C ++ 20

Il problema si è notevolmente semplificato con P0907: i numeri interi firmati sono il complemento di due e la dicitura finale P1236 che è stata votata nello standard C ++ 20. Ora, la risposta è il più semplice possibile:

template<std::unsigned_integral T>
constexpr auto cast_to_signed_integer(T const value) {
    return static_cast<std::make_signed_t<T>>(value);
}

Questo è tutto. Un static_castcast in stile C è finalmente garantito per fare la cosa di cui hai bisogno per questa domanda, e la cosa che molti programmatori pensavano che facesse sempre.

Risposta in C ++ 17

In C ++ 17, le cose sono molto più complicate. Abbiamo a che fare con tre possibili rappresentazioni di interi (complemento a due, complemento a uno e grandezza del segno). Anche nel caso in cui sappiamo che deve essere un complemento a due perché abbiamo verificato l'intervallo di valori possibili, la conversione di un valore al di fuori dell'intervallo dell'intero con segno in quell'intero con segno fornisce comunque un risultato definito dall'implementazione. Dobbiamo usare trucchi come abbiamo visto in altre risposte.

Innanzitutto, ecco il codice su come risolvere il problema genericamente:

template<typename T, typename = std::enable_if_t<std::is_unsigned_v<T>>>
constexpr auto cast_to_signed_integer(T const value) {
    using result = std::make_signed_t<T>;
    using result_limits = std::numeric_limits<result>;
    if constexpr (result_limits::min() + 1 != -result_limits::max()) {
        if (value == static_cast<T>(result_limits::max()) + 1) {
            throw std::runtime_error("Cannot convert the maximum possible unsigned to a signed value on this system");
        }
    }
    if (value <= result_limits::max()) {
        return static_cast<result>(value);
    } else {
        using promoted_unsigned = std::conditional_t<sizeof(T) <= sizeof(unsigned), unsigned, T>;
        using promoted_signed = std::make_signed_t<promoted_unsigned>;
        constexpr auto shift_by_window = [](auto x) {
            // static_cast to avoid conversion warning
            return x - static_cast<decltype(x)>(result_limits::max()) - 1;
        };
        return static_cast<result>(
            shift_by_window( // shift values from common range to negative range
                static_cast<promoted_signed>(
                    shift_by_window( // shift large values into common range
                        static_cast<promoted_unsigned>(value) // cast to avoid promotion to int
                    )
                )
            )
        );
    }
}

Questo ha qualche cast in più rispetto alla risposta accettata, e questo è per garantire che non ci siano avvisi di mancata corrispondenza firmati / non firmati dal compilatore e per gestire correttamente le regole di promozione di interi.

Innanzitutto abbiamo un caso speciale per i sistemi che non sono complemento a due (e quindi dobbiamo gestire il valore massimo possibile specialmente perché non ha nulla su cui mappare). Dopodiché, arriviamo all'algoritmo reale.

La seconda condizione di primo livello è semplice: sappiamo che il valore è minore o uguale al valore massimo, quindi si adatta al tipo di risultato. La terza condizione è un po 'più complicata anche con i commenti, quindi alcuni esempi potrebbero probabilmente aiutare a capire perché ogni affermazione è necessaria.

Base concettuale: la linea dei numeri

In primo luogo, qual è questo windowconcetto? Considera la seguente linea numerica:

   |   signed   |
<.........................>
          |  unsigned  |

Si scopre che per i numeri interi con complemento a due, è possibile dividere il sottoinsieme della linea numerica che può essere raggiunta da entrambi i tipi in tre categorie di uguale dimensione:

- => signed only
= => both
+ => unsigned only

<..-------=======+++++++..>

Ciò può essere facilmente dimostrato considerando la rappresentazione. Un intero senza segno inizia da 0e utilizza tutti i bit per aumentare il valore in potenze di 2. Un intero con segno è esattamente lo stesso per tutti i bit tranne il bit di segno, che vale -(2^position)invece di 2^position. Ciò significa che per tutti i n - 1bit, rappresentano gli stessi valori. Quindi, gli interi senza segno hanno un bit normale in più, che raddoppia il numero totale di valori (in altre parole, ci sono tanti valori con quel bit impostato quanti ne sono senza). La stessa logica vale per gli interi con segno, tranne per il fatto che tutti i valori con quel bit impostato sono negativi.

Le altre due rappresentazioni di interi legali, il complemento a uno e la grandezza del segno, hanno tutti gli stessi valori dei numeri interi del complemento a due tranne uno: il valore più negativo. Il C ++ definisce tutto sui tipi interi, tranne reinterpret_cast(e il C ++ 20 std::bit_cast), in termini di intervallo di valori rappresentabili, non in termini di rappresentazione dei bit. Ciò significa che la nostra analisi sarà valida per ciascuna di queste tre rappresentazioni fintanto che non cercheremo mai di creare la rappresentazione trappola. Il valore senza segno che verrebbe mappato a questo valore mancante è piuttosto sfortunato: quello proprio al centro dei valori senza segno. Fortunatamente, la nostra prima condizione controlla (in fase di compilazione) se esiste una tale rappresentazione, e poi la gestisce specialmente con un controllo di runtime.

La prima condizione gestisce il caso in cui ci troviamo nella =sezione, il che significa che siamo nella regione di sovrapposizione in cui i valori in uno possono essere rappresentati nell'altro senza modifiche. La shift_by_windowfunzione nel codice sposta tutti i valori verso il basso della dimensione di ciascuno di questi segmenti (dobbiamo sottrarre il valore massimo quindi sottrarre 1 per evitare problemi di overflow aritmetico). Se siamo al di fuori di quella regione (siamo nella +regione), dobbiamo saltare di una dimensione della finestra. Questo ci colloca nell'intervallo di sovrapposizione, il che significa che possiamo convertire in sicurezza da non firmato a firmato perché non vi è alcuna variazione di valore. Tuttavia, non abbiamo ancora finito perché abbiamo mappato due valori senza segno a ciascun valore con segno. Pertanto, dobbiamo passare alla finestra successiva (il file- regione) in modo da avere di nuovo una mappatura unica.

Ora, questo ci dà un risultato congruente mod UINT_MAX + 1, come richiesto nella domanda? UINT_MAX + 1è equivalente a 2^n, dove nè il numero di bit nella rappresentazione del valore. Il valore che usiamo per la dimensione della nostra finestra è uguale a 2^(n - 1)(l'indice finale in una sequenza di valori è uno in meno della dimensione). Sottraiamo quel valore due volte, il che significa che sottraiamo 2 * 2^(n - 1)che è uguale a 2^n. L'addizione e la sottrazione xè un no-op nella mod aritmetica x, quindi non abbiamo modificato il valore originale mod 2^n.

Gestire correttamente le promozioni intere

Perché questa è una funzione generica e non solo inte unsigned, dobbiamo anche occuparci di regole di promozione integrale. Ci sono due casi forse interessanti: uno in cui shortè più piccolo di inte uno in cui ha shortle stesse dimensioni di int.

Esempio: shortminore diint

Se shortè più piccolo di int(comune sulle piattaforme moderne), sappiamo anche che unsigned shortpuò rientrare in un int, il che significa che qualsiasi operazione su di esso avverrà effettivamente in int, quindi eseguiamo esplicitamente il cast al tipo promosso per evitarlo. La nostra affermazione finale è piuttosto astratta e diventa più facile da capire se sostituiamo valori reali. Per il nostro primo caso interessante, senza perdita di generalità, consideriamo un 16 bit shorte un 17 bit int(che è ancora consentito dalle nuove regole, e significherebbe semplicemente che almeno uno di questi due tipi interi ha alcuni bit di riempimento ):

constexpr auto shift_by_window = [](auto x) {
    return x - static_cast<decltype(x)>(32767) - 1;
};
return static_cast<int16_t>(
    shift_by_window(
        static_cast<int17_t>(
            shift_by_window(
                static_cast<uint17_t>(value)
            )
        )
    )
);

Risoluzione per il massimo valore senza segno a 16 bit possibile

constexpr auto shift_by_window = [](auto x) {
    return x - static_cast<decltype(x)>(32767) - 1;
};
return int16_t(
    shift_by_window(
        int17_t(
            shift_by_window(
                uint17_t(65535)
            )
        )
    )
);

Semplifica a

return int16_t(
    int17_t(
        uint17_t(65535) - uint17_t(32767) - 1
    ) -
    int17_t(32767) -
    1
);

Semplifica a

return int16_t(
    int17_t(uint17_t(32767)) -
    int17_t(32767) -
    1
);

Semplifica a

return int16_t(
    int17_t(32767) -
    int17_t(32767) -
    1
);

Semplifica a

return int16_t(-1);

Mettiamo il più grande possibile non firmato e torniamo -1, successo!

Esempio: shortstesse dimensioni diint

Se shortè della stessa dimensione di int(non comune sulle piattaforme moderne), la regola di promozione integrale è leggermente diversa. In questo caso, shortpromuove a inte unsigned shortpromuove a unsigned. Fortunatamente, eseguiamo esplicitamente il cast di ogni risultato nel tipo in cui vogliamo eseguire il calcolo, quindi non ci saranno promozioni problematiche. Senza alcuna perdita di generalità, consideriamo un 16 bit shorte uno 16 bit int:

constexpr auto shift_by_window = [](auto x) {
    return x - static_cast<decltype(x)>(32767) - 1;
};
return static_cast<int16_t>(
    shift_by_window(
        static_cast<int16_t>(
            shift_by_window(
                static_cast<uint16_t>(value)
            )
        )
    )
);

Risoluzione per il massimo valore senza segno a 16 bit possibile

auto x = int16_t(
    uint16_t(65535) - uint16_t(32767) - 1
);
return int16_t(
    x - int16_t(32767) - 1
);

Semplifica a

return int16_t(
    int16_t(32767) - int16_t(32767) - 1
);

Semplifica a

return int16_t(-1);

Mettiamo il più grande possibile non firmato e torniamo -1, successo!

Che cosa succede se ho appena interessa inte unsignede non mi importa di avvertimenti, come la domanda originale?

constexpr int cast_to_signed_integer(unsigned const value) {
    using result_limits = std::numeric_limits<int>;
    if constexpr (result_limits::min() + 1 != -result_limits::max()) {
        if (value == static_cast<unsigned>(result_limits::max()) + 1) {
            throw std::runtime_error("Cannot convert the maximum possible unsigned to a signed value on this system");
        }
    }
    if (value <= result_limits::max()) {
        return static_cast<int>(value);
    } else {
        constexpr int window = result_limits::min();
        return static_cast<int>(value + window) + window;
    }
}

Guardalo dal vivo

https://godbolt.org/z/74hY81

Qui vediamo che clang, gcc e icc non generano codice per caste cast_to_signed_integer_basicat -O2e -O3, e MSVC non genera codice at /O2, quindi la soluzione è ottimale.


3

Puoi dire esplicitamente al compilatore cosa vuoi fare:

int unsigned_to_signed(unsigned n) {
  if (n > INT_MAX) {
    if (n <= UINT_MAX + INT_MIN) {
      throw "no result";
    }
    return static_cast<int>(n + INT_MIN) - (UINT_MAX + INT_MIN + 1);
  } else {
    return static_cast<int>(n);
  }
}

Compila con gcc 4.7.2for x86_64-linux( g++ -O -S test.cpp) a

_Z18unsigned_to_signedj:
    movl    %edi, %eax
    ret

UINT_MAXè un'espressione di tipo unsigned int, e questo rende il tuo tutto static_cast<int>(n + INT_MIN) - (UINT_MAX + INT_MIN + 1)di quel tipo. Dovrebbe essere possibile risolverlo, tuttavia, e mi aspetto che venga compilato allo stesso modo.

2

Se xè il nostro input ...

Se x > INT_MAX, vogliamo trovare una costante ktale che 0< x - k*INT_MAX< INT_MAX.

Questo è facile - unsigned int k = x / INT_MAX;. Quindi, lasciaunsigned int x2 = x - k*INT_MAX;

Ora possiamo eseguire il cast x2in intmodo sicuro. Permettereint x3 = static_cast<int>(x2);

Ora vogliamo sottrarre qualcosa come UINT_MAX - k * INT_MAX + 1da x3, if k > 0.

Ora, su un sistema a 2 complementi, a patto che x > INT_MAXfunzioni per:

unsigned int k = x / INT_MAX;
x -= k*INT_MAX;
int r = int(x);
r += k*INT_MAX;
r -= UINT_MAX+1;

Si noti che UINT_MAX+1è garantito zero in C ++, la conversione a int era un noop e l'abbiamo sottratta e k*INT_MAXquindi aggiunta di nuovo su "lo stesso valore". Quindi un ottimizzatore accettabile dovrebbe essere in grado di cancellare tutte quelle stupidaggini!

Questo lascia il problema x > INT_MAXo no. Bene, creiamo 2 rami, uno con x > INT_MAXe uno senza. Quello senza fa un cast stretto, che il compilatore ottimizza a noop. Quello con ... fa un noop dopo che l'ottimizzatore è terminato. L'ottimizzatore intelligente realizza entrambi i rami alla stessa cosa e rilascia il ramo.

Problemi: se UINT_MAXè molto grande rispetto a INT_MAX, quanto sopra potrebbe non funzionare. Lo presumo k*INT_MAX <= UINT_MAX+1implicitamente.

Probabilmente potremmo attaccarlo con alcune enumerazioni come:

enum { divisor = UINT_MAX/INT_MAX, remainder = UINT_MAX-divisor*INT_MAX };

che funzionano a 2 e 1 su un sistema di complemento 2, credo (siamo garantiti che la matematica funzioni? È complicato ...), e fare logica basata su questi che ottimizzano facilmente su sistemi di complemento non 2 ...

Questo apre anche il caso di eccezione. È possibile solo se UINT_MAX è molto più grande di (INT_MIN-INT_MAX), quindi puoi inserire il tuo codice di eccezione in un blocco if chiedendo esattamente quella domanda in qualche modo e non ti rallenterà su un sistema tradizionale.

Non sono esattamente sicuro di come costruire quelle costanti in fase di compilazione per gestirle correttamente.


UINT_MAXnon può essere piccolo rispetto a INT_MAX, perché la specifica garantisce che ogni int con segno positivo sia rappresentabile come int senza segno. Ma UINT_MAX+1è zero su ogni sistema; l'aritmetica senza segno è sempre modulo UINT_MAX+1. Tuttavia potrebbe esserci un nucleo di un approccio praticabile qui ...
Nemo

@Nemo Sto solo seguendo questo thread, quindi scusa la mia domanda potenzialmente ovvia: la tua affermazione " UINT_MAX+1è zero su ogni sistema" stabilito nella specifica '03? Se è così, c'è una sottosezione specifica in cui dovrei guardare? Grazie.
WhozCraig

@WhozCraig: Sezione 3.9.1 paragrafo 4: "Gli interi senza segno, dichiarati senza segno, devono obbedire alle leggi dell'aritmetica modulo 2 ^ n dove n è il numero di bit nella rappresentazione del valore di quella particolare dimensione dell'intero", con una nota a piè di pagina che dice "Ciò implica che l'aritmetica senza segno non va in overflow 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 intero senza segno risultante." Fondamentalmente non firmato è specificato per funzionare nel modo desiderato / previsto.
Nemo

@Nemo Grazie. davvero apprezzato.
WhozCraig

1

std::numeric_limits<int>::is_moduloè una costante del tempo di compilazione. quindi puoi usarlo per la specializzazione dei modelli. problema risolto, almeno se il compilatore gioca con l'inlining.

#include <limits>
#include <stdexcept>
#include <string>

#ifdef TESTING_SF
    bool const testing_sf = true;
#else
    bool const testing_sf = false;
#endif

// C++ "extensions"
namespace cppx {
    using std::runtime_error;
    using std::string;

    inline bool hopefully( bool const c ) { return c; }
    inline bool throw_x( string const& s ) { throw runtime_error( s ); }

}  // namespace cppx

// C++ "portability perversions"
namespace cppp {
    using cppx::hopefully;
    using cppx::throw_x;
    using std::numeric_limits;

    namespace detail {
        template< bool isTwosComplement >
        int signed_from( unsigned const n )
        {
            if( n <= unsigned( numeric_limits<int>::max() ) )
            {
                return static_cast<int>( n );
            }

            unsigned const u_max = unsigned( -1 );
            unsigned const u_half = u_max/2 + 1;

            if( n == u_half )
            {
                throw_x( "signed_from: unsupported value (negative max)" );
            }

            int const i_quarter = static_cast<int>( u_half/2 );
            int const int_n1 = static_cast<int>( n - u_half );
            int const int_n2 = int_n1 - i_quarter;
            int const int_n3 = int_n2 - i_quarter;

            hopefully( n == static_cast<unsigned>( int_n3 ) )
                || throw_x( "signed_from: range error" );

            return int_n3;
        }

        template<>
        inline int signed_from<true>( unsigned const n )
        {
            return static_cast<int>( n );
        }
    }    // namespace detail

    inline int signed_from( unsigned const n )
    {
        bool const is_modulo = numeric_limits< int >::is_modulo;
        return detail::signed_from< is_modulo && !testing_sf >( n );
    }
}    // namespace cppp

#include <iostream>
using namespace std;
int main()
{
    int const x = cppp::signed_from( -42u );
    wcout << x << endl;
}


EDIT : corretto codice per evitare possibili trap su macchine non modulari (ne esiste solo uno, vale a dire le versioni configurate in modo arcaico di Unisys Clearpath). Per semplicità questo viene fatto non supportando il valore -2 n -1 dove n è il numero di intbit di valore, su tale macchina (cioè, sul Clearpath). in pratica questo valore non sarà supportato dalla macchina (cioè, con rappresentazione di segno e grandezza o complemento a 1).


1

Penso che il tipo int sia di almeno due byte, quindi INT_MIN e INT_MAX potrebbero cambiare in piattaforme diverse.

Tipi fondamentali

≤climits≥ intestazione


Sono maledetto a usare un compilatore per il 6809 che è configurato con "-mint8" per impostazione predefinita, dove int è 8 bit :-( (questo è l'ambiente di sviluppo per Vectrex) long è 2 byte, long long è 4 byte e Non ho idea di cosa sia breve ...
Graham Toal

1

I miei soldi sono sull'uso di memcpy. Qualsiasi compilatore decente sa come ottimizzarlo:

#include <stdio.h>
#include <memory.h>
#include <limits.h>

static inline int unsigned_to_signed(unsigned n)
{
    int result;
    memcpy( &result, &n, sizeof(result));
    return result;
}

int main(int argc, const char * argv[])
{
    unsigned int x = UINT_MAX - 1;
    int xx = unsigned_to_signed(x);
    return xx;
}

Per me (Xcode 8.3.2, Apple LLVM 8.1, -O3), questo produce:

_main:                                  ## @main
Lfunc_begin0:
    .loc    1 21 0                  ## /Users/Someone/main.c:21:0
    .cfi_startproc
## BB#0:
    pushq    %rbp
Ltmp0:
    .cfi_def_cfa_offset 16
Ltmp1:
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
Ltmp2:
    .cfi_def_cfa_register %rbp
    ##DEBUG_VALUE: main:argc <- %EDI
    ##DEBUG_VALUE: main:argv <- %RSI
Ltmp3:
    ##DEBUG_VALUE: main:x <- 2147483646
    ##DEBUG_VALUE: main:xx <- 2147483646
    .loc    1 24 5 prologue_end     ## /Users/Someone/main.c:24:5
    movl    $-2, %eax
    popq    %rbp
    retq
Ltmp4:
Lfunc_end0:
    .cfi_endproc

1
Questo non risponde alla domanda, poiché la rappresentazione binaria di un non firmato non è garantita dallo standard per corrispondere alla rappresentazione firmata.
TLW
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.