Diciamo che si sta utilizzando <cstdint>
e tipi come std::uint8_t
e std::uint16_t
, e vogliono fare operazioni come +=
e *=
su di loro. Ti piacerebbe che l'aritmetica su questi numeri si articolasse in modo modulare, come tipico in C/C++. Questo di solito funziona, e si trova sperimentalmente funziona con std::uint8_t
, std::uint32_t
e std::uint64_t
, ma non std::uint16_t
.Qual è il miglior modo C++ per moltiplicare in modo modulare gli interi senza segno?
In particolare, la moltiplicazione con std::uint16_t
a volte fallisce in modo spettacolare, con build ottimizzati che producono tutti i tipi di risultati strani. La ragione? Comportamento indefinito a causa di overflow di interi con segno. Il compilatore si sta ottimizzando basandosi sul presupposto che il comportamento non definito non si verifichi, e quindi avvia pezzi di codice di eliminazione dal programma. Il comportamento non definito specifica è la seguente:
std::uint16_t x = UINT16_C(0xFFFF);
x *= x;
Il motivo è regole di promozione C++ s 'e il fatto che voi, come quasi tutti gli altri in questi giorni, si utilizza una piattaforma su cui std::numeric_limits<int>::digits == 31
. Cioè, int
è 32 bit (digits
conta i bit ma non il bit di segno). x
viene promosso a signed int
, nonostante sia senza firma e 0xFFFF * 0xFFFF
overflow per aritmetica con segno a 32 bit.
demo del problema generale:
// Compile on a recent version of clang and run it:
// clang++ -std=c++11 -O3 -Wall -fsanitize=undefined stdint16.cpp -o stdint16
#include <cinttypes>
#include <cstdint>
#include <cstdio>
int main()
{
std::uint8_t a = UINT8_MAX; a *= a; // OK
std::uint16_t b = UINT16_MAX; b *= b; // undefined!
std::uint32_t c = UINT32_MAX; c *= c; // OK
std::uint64_t d = UINT64_MAX; d *= d; // OK
std::printf("%02" PRIX8 " %04" PRIX16 " %08" PRIX32 " %016" PRIX64 "\n",
a, b, c, d);
return 0;
}
si otterrà un bel errore:
main.cpp:11:55: runtime error: signed integer overflow: 65535 * 65535
cannot be represented in type 'int'
Il modo per evitare questo, naturalmente, è quello di gettare almeno unsigned int
prima di moltiplicare. Solo il caso esatto in cui il numero di bit del tipo unsigned corrisponde esattamente alla metà del numero di bit di int
è problematico. Qualsiasi più piccolo comporterebbe la moltiplicazione non essendo in grado di traboccare, come con std::uint8_t
; più grande risulterebbe nel tipo esattamente mappato a uno dei gradi di promozione, come con std::uint64_t
corrispondente a unsigned long
o unsigned long long
a seconda della piattaforma.
Ma questo fa davvero schifo: richiede sapere quale tipo è problematico in base alle dimensioni di int
sulla piattaforma corrente. C'è un modo migliore con il quale un comportamento indefinito con moltiplicazione di interi senza segno può essere evitato senza labirinti #if
?
Immagino che "lanciare sempre entrambi su" unsigned long long "non è una buona idea? –
Oltre la metà dei bit di 'int', ma in realtà non così grandi, sarebbe anche problematica (mai vista un'architettura in cui esista un tipo di questo tipo) –
@TC: Sembra molto inefficiente. I multipli a 64 bit possono essere lenti. –