I tipi a larghezza variabile sono stati sostituiti da tipi fissi nella moderna C?


21

Mi sono imbattuto in un punto interessante oggi in una recensione su Code Review . @Veedrac ha raccomandato in questa risposta che tipi di dimensioni variabili (es. intE long) vengano sostituiti con tipi di dimensioni fisse come uint64_te uint32_t. Citazione dai commenti di tale risposta:

Le dimensioni di int e long (e quindi i valori che possono contenere) dipendono dalla piattaforma. D'altra parte, int32_t è sempre lungo 32 bit. L'uso di int significa semplicemente che il tuo codice funziona in modo diverso su piattaforme diverse, che generalmente non è quello che desideri.

Il ragionamento alla base dello standard che non fissa i tipi comuni è parzialmente spiegato qui da @supercat. C è stato scritto per essere trasportabile su più architetture, al contrario dell'assemblaggio che di solito era usato per la programmazione dei sistemi in quel momento.

Penso che l'intenzione progettuale fosse originariamente che ogni tipo diverso da int fosse la cosa più piccola in grado di gestire numeri di varie dimensioni, e che int fosse la dimensione "generica" ​​più pratica che potesse gestire +/- 32767.

Per quanto mi riguarda, ho sempre usato inte non ero molto preoccupato per le alternative. Ho sempre pensato che fosse il più tipo con le migliori prestazioni, fine della storia. L'unico posto in cui pensavo che la larghezza fissa fosse utile è quando si codificano i dati per l'archiviazione o per il trasferimento in rete. Raramente ho visto tipi di larghezza fissa nel codice scritto da altri.

Sono bloccato negli anni '70 o c'è davvero una logica per l'utilizzo intnell'era del C99 e oltre?


1
Una parte della gente imita solo gli altri. Credo che la maggior parte del codice a bit fisso sia stata resa priva di coscienza. Nessun motivo per impostare la dimensione né per non farlo. Ho il codice creato principalmente su piattaforme a 16 bit (MS-DOS e Xenix degli anni '80), che oggi vengono compilate ed eseguite su qualsiasi 64 e benefici della nuova dimensione e indirizzamento delle parole, semplicemente compilandolo. Vale a dire che la serializzazione per esportare / importare dati è un progetto di architettura molto importante per mantenerlo portatile.
Luciano,

Risposte:


7

Esiste un mito comune e pericoloso in cui tipi come i uint32_tprogrammatori di salvataggio non devono preoccuparsi delle dimensioni di int. Mentre sarebbe utile se il Comitato Standards definisse un mezzo per dichiarare numeri interi con semantica indipendente dalla macchina, tipi senza segno come uint32_thanno semantica che sono troppo larghi per consentire la scrittura del codice in modo pulito e portatile; inoltre, i tipi int32con segno come hanno la semantica che sono per molte applicazioni definiti in modo inutilmente stretto e quindi precludono quelle che sarebbero altrimenti utili ottimizzazioni.

Considera, ad esempio:

uint32_t upow(uint32_t n, uint32_t exponent)
{
  while(exponent--)
    n*=n;
  return n;
}

int32_t spow(int32_t n, uint32_t exponent)
{
  while(exponent--)
    n*=n;
  return n;
}

Su macchine dove into non può tenere 4294967295, oppure possono contenere 18446744065119617025 sarà definita la prima funzione per tutti i valori di ne exponent, e il suo comportamento non sarà influenzato dalle dimensioni del int; inoltre, lo standard non richiederà che produca un comportamento diverso su macchine con qualsiasi dimensione di int Alcuni valori di ne exponent, tuttavia, lo farà invocare un comportamento indefinito su macchine in cui 4294967295 è rappresentabile come un int18446744065119617025 ma non lo è.

La seconda funzione produrrà un comportamento indefinito per alcuni valori di ne exponentsu macchine in cui intnon può contenere 4611686014132420609, ma produrrà un comportamento definito per tutti i valori di ne exponentsu tutte le macchine in cui può (le specifiche int32_timplicano che il comportamento di avvolgimento del complemento a due sulle macchine in cui è presente è più piccolo di int).

Storicamente, anche se lo Standard non ha detto nulla su ciò che i compilatori dovrebbero fare con l' intoverflow upow, i compilatori avrebbero costantemente prodotto lo stesso comportamento di se intfosse stato abbastanza grande da non traboccare. Sfortunatamente, alcuni compilatori più recenti potrebbero cercare di "ottimizzare" i programmi eliminando comportamenti non previsti dalla norma.


3
Chiunque desideri implementare manualmente pow, ricorda questo codice è solo un esempio e non soddisfa exponent=0!
Mark Hurd,

1
Penso che dovresti usare l'operatore di decremento prefisso e non il postfisso, attualmente sta eseguendo 1 moltiplicazione aggiuntiva, ad esempio exponent=1si tradurrà in n una moltiplicazione da sola una volta, poiché il decremento viene eseguito dopo il controllo, se l'incremento viene eseguito prima del controllo ( cioè --exponent), non verrà eseguita alcuna moltiplicazione e verrà restituito n stesso.
ALXGTV,

2
@MarkHurd: La funzione ha un nome mediocre, poiché ciò che effettivamente calcola è N^(2^exponent), ma i calcoli della forma N^(2^exponent)sono spesso usati nel calcolo delle funzioni di esponenziazione, e l'esponenziazione mod-4294967296 è utile per cose come calcolare l'hash della concatenazione di due stringhe la cui gli hash sono noti.
supercat

1
@ALXGTV: La funzione doveva essere illustrativa di qualcosa che calcolava qualcosa legato al potere. Ciò che calcola effettivamente è N ^ (esponente 2 ^), che fa parte del calcolo efficiente dell'esponente N ^, e potrebbe anche fallire anche se N è piccolo (la moltiplicazione ripetuta di a uint32_tper 31 non produrrà mai UB, ma l'efficienza il modo di calcolare 31 ^ N prevede calcoli di 31 ^ (2 ^ N), che lo faranno.
supercat

Non penso che questo sia un buon argomento. Lo scopo non è rendere le funzioni definite per tutti gli input, sensibili o meno; è in grado di ragionare su dimensioni e troppo pieno. int32_ta volte avere definito overflow e talvolta no, che è quello che sembra stia menzionando, sembra di minima importanza rispetto al fatto che mi fa ragionare sulla prevenzione dell'overflow in primo luogo. E se si desidera un overflow definito, è probabile che si desideri che il modulo comporti un valore fisso, quindi si utilizzano comunque tipi a larghezza fissa.
Veedrac,

4

Per valori strettamente correlati ai puntatori (e quindi alla quantità di memoria indirizzabile) come dimensioni del buffer, indici di array e Windows ' lParam, ha senso avere un tipo intero con una dimensione dipendente dall'architettura. Pertanto, i tipi di dimensioni variabili sono ancora utili. Questo è il motivo per cui abbiamo i typedef size_t, ptrdiff_t, intptr_t, ecc Essi hanno essere typedef perché nessuno di built-in C interi tipi deve essere puntatore dimensioni.

Quindi la domanda è se char, short, int, long, e long longsono ancora utili.

IME, è ancora comune che i programmi C e C ++ utilizzino intper la maggior parte delle cose. E la maggior parte delle volte (vale a dire, quando i tuoi numeri sono compresi nell'intervallo ± 32 767 e non hai requisiti di prestazione rigorosi), funziona perfettamente.

Ma cosa succede se è necessario lavorare con numeri nell'intervallo 17-32 bit (come le popolazioni delle grandi città)? Potresti usare int, ma questo sarebbe hard-coding di una dipendenza dalla piattaforma. Se si desidera aderire rigorosamente allo standard, è possibile utilizzare long, che è garantito per essere almeno 32 bit.

Il problema è che lo standard C non specifica alcuna dimensione massima per un tipo intero. Esistono implementazioni su long64 bit, che raddoppiano l'utilizzo della memoria. E se questi longsono elementi di un array con milioni di elementi, distruggerai la memoria come un matto.

Quindi, né intne longè un tipo adatto da utilizzare qui se si desidera che il programma sia sia cross-platform e la memoria-efficiente. Enter int_least32_t.

  • Il compilatore I16L32 fornisce 32 bit long, evitando i problemi di troncamento diint
  • Il compilatore I32L64 ti dà un 32-bit int, evitando la memoria sprecata del 64-bit long.
  • Il compilatore I36L72 ti dà un 36-bit int

OTOH, supponiamo che non abbiate bisogno di numeri enormi o array enormi ma che abbiate bisogno di velocità. E intpotrebbe essere abbastanza grande su tutte le piattaforme, ma non è necessariamente il tipo più veloce: i sistemi a 64 bit di solito hanno ancora 32 bit int. Ma è possibile utilizzare int_fast16_te ottenere il tipo di “più veloce”, che si tratti di int, longo long long.

Quindi, ci sono casi d'uso pratici per i tipi da <stdint.h>. I tipi interi standard non significano nulla. Soprattutto long, che può essere a 32 o 64 bit e può essere o meno abbastanza grande da contenere un puntatore, a seconda del capriccio degli autori del compilatore.


Un problema con tipi come uint_least32_tè che le loro interazioni con altri tipi sono ancora più debolmente specificate di quelle di uint32_t. IMHO, lo standard dovrebbe definire tipi simili uwrap32_te unum32_t, con la semantica che qualsiasi compilatore che definisce il tipo uwrap32_t, deve promuovere come tipo senza segno essenzialmente negli stessi casi in cui verrebbe promosso se intfosse a 32 bit e qualsiasi compilatore che definisce il tipo unum32_tdeve garantire che le promozioni aritmetiche di base lo convertono sempre in un tipo firmato in grado di mantenerne il valore.
supercat

Inoltre, lo standard potrebbe anche definire i tipi di archiviazione e aliasing compatibili con intN_te uintN_t, e i cui comportamenti definiti sarebbero coerenti con intN_te uintN_t, ma che garantirebbe ai compilatori un po 'di libertà nel caso in cui al codice vengano assegnati valori al di fuori del loro intervallo [consentendo semantiche simili a quelle che erano forse inteso per uint_least32_t, ma senza incertezze come se l'aggiunta di a uint_least16_te an int32_tprodurrebbe un risultato con o senza segno.
supercat
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.