Un divieto "lungo" ha senso?

105

Nel mondo odierno multipiattaforma C ++ (o C) abbiamo avere :

Data model  | short |   int |   long | long long | pointers/size_t  | Sample operating systems
... 
LLP64/IL32P64   16      32      32     64           64                Microsoft Windows (x86-64 and IA-64)
LP64/I32LP64    16      32      64     64           64                Most Unix and Unix-like systems, e.g. Solaris, Linux, BSD, and OS X; z/OS
...

Ciò significa oggi, è che per ogni intero "comune" (firmato), int sarà sufficiente e può ancora essere usato come tipo intero predefinito quando si scrive codice dell'applicazione C ++. Inoltre - per gli attuali scopi pratici - avrà una dimensione consistente su tutte le piattaforme.

Se un caso d'uso richiede almeno 64 bit, oggi possiamo usare long long , anche se probabilmente si usa uno dei tipi di specificazione dei bitness o il tipo __int64 potrebbe avere più senso.

Questo valore lascia long nel mezzo e stiamo considerando di vietare a titolo definitivo l'utilizzo di long dal nostro codice applicazione .

Questo avrebbe senso , o c'è un motivo per usare long nel moderno codice C ++ (o C) che deve essere eseguito su più piattaforme? (la piattaforma è desktop, dispositivi mobili, ma non cose come microcontrollori, DSP ecc.)

Link di sfondo possibilmente interessanti:

posta Martin Ba 05.05.2016 - 22:14
fonte

4 risposte

17

L'unica ragione per cui oggi utilizzerei long è quando si chiama o si implementa un'interfaccia esterna che lo utilizza.

Come dici tu nel tuo post breve e int hanno caratteristiche ragionevolmente stabili su tutte le principali piattaforme desktop / server / mobili oggi e non vedo alcuna ragione per cambiare nel prossimo futuro. Quindi vedo poche ragioni per evitarli in generale.

long d'altra parte è un casino. Su tutti i sistemi a 32 bit sono consapevole che aveva le seguenti caratteristiche.

  1. Aveva esattamente la dimensione di 32 bit.
  2. Aveva le stesse dimensioni di un indirizzo di memoria.
  3. Aveva le stesse dimensioni della più grande unità di dati che potesse essere contenuta in un normale registro e lavorasse con una singola istruzione.

Grandi quantità di codice sono state scritte in base a una o più di queste caratteristiche. Tuttavia con il passaggio a 64-bit non è stato possibile conservarli tutti. Piattaforme simil-Unix per LP64 che conserva le caratteristiche 2 e 3 al costo della caratteristica 1. Win64 è andato per LLP64 che ha mantenuto la caratteristica 1 al costo delle caratteristiche 2 e 3. Il risultato è che non è più possibile fare affidamento su nessuna di queste caratteristiche e che IMO lascia pochi motivi per usare long .

Se vuoi un tipo con dimensioni esattamente a 32 bit, devi utilizzare int32_t .

Se vuoi un tipo delle stesse dimensioni di un puntatore, devi usare intptr_t (o meglio uintptr_t ).

Se vuoi un tipo che è l'elemento più grande che può essere lavorato in un singolo registro / istruzione, sfortunatamente non penso che lo standard ne fornisca uno. size_t dovrebbe essere giusto sulla maggior parte delle piattaforme comuni, ma non sarebbe su x32 .

P.S.

Non mi preoccuperei dei tipi "veloce" o "minimo". I tipi "minimi" sono importanti solo se ti interessa la portabilità di architetture davvero oscure dove CHAR_BIT != 8 . La dimensione dei tipi "veloci" nella pratica sembra essere piuttosto arbitary. Linux sembra renderli almeno delle stesse dimensioni del puntatore, che è stupido su piattaforme a 64 bit con supporto rapido a 32 bit come x86-64 e arm64. IIRC iOS li rende il più piccolo possibile. Non sono sicuro di cosa facciano gli altri sistemi.

P.P.S

Un motivo per usare unsigned long (ma non per% long ) è perché è garantito il comportamento del modulo. Sfortunatamente, a causa delle avvincenti regole di promozione di C, i tipi non firmati più piccoli di int non hanno il comportamento del modulo.

Su tutte le piattaforme principali oggi uint32_t ha la stessa dimensione o maggiore di int e quindi ha un comportamento del modulo. Tuttavia ci sono stati storicamente e potrebbero esserci teoricamente nelle piattaforme future dove int è 64-bit e quindi uint32_t non ha comportamento di modulo.

Personalmente direi che è meglio entrare nell'abitudine di forzare il comportamento del modulo usando "1u *" o "0u +" all'inizio delle equazioni, poiché questo funzionerà per qualsiasi dimensione di tipo senza segno.

    
risposta data 06.05.2016 - 03:08
fonte
201

Come accennato nella tua domanda, il software moderno riguarda l'interoperabilità tra piattaforme e sistemi su Internet. Gli standard C e C ++ forniscono intervalli per le dimensioni dei caratteri interi, non per le dimensioni specifiche (in contrasto con linguaggi come Java e C #).

Per assicurarti che il tuo software compilato su piattaforme diverse funzioni con gli stessi dati allo stesso modo e per garantire che altri software possano interagire con il tuo software utilizzando le stesse dimensioni, dovresti usare dimensioni fisse interi.

Inserisci <cstdint> che fornisce esattamente questo ed è un'intestazione standard che richiede tutte le piattaforme di compilazione e di libreria standard fornire. Nota: questa intestazione era richiesta solo in C ++ 11, ma molte implementazioni di librerie precedenti lo hanno comunque fornito.

Vuoi un intero senza segno a 64 bit? Usa uint64_t . Numero intero a 32 bit con segno? Usa int32_t . Mentre i tipi nell'intestazione sono opzionali, le piattaforme moderne dovrebbero supportare tutti i tipi definiti in quell'intestazione.

A volte è necessaria una larghezza di bit specifica, ad esempio, in una struttura dati utilizzata per comunicare con altri sistemi. Altre volte non lo è. Per situazioni meno rigorose, <cstdint> fornisce tipi con larghezza minima.

Ci sono varianti meno : int_leastXX_t sarà un tipo intero di XX bit minimi. Userà il tipo più piccolo che fornisce XX bit, ma il tipo può essere più grande del numero specificato di bit. In pratica, questi sono tipicamente gli stessi dei tipi descritti sopra che danno il numero esatto di bit.

Ci sono anche varianti fast : int_fastXX_t è almeno XX bit, ma dovrebbe usare un tipo che funzioni velocemente su una piattaforma particolare. La definizione di "veloce" in questo contesto non è specificata. Tuttavia, in pratica, questo in genere significa che un tipo più piccolo della dimensione del registro di una CPU può essere alias di un tipo di dimensione del registro della CPU. Ad esempio, l'intestazione di Visual C ++ 2015 specifica che int_fast16_t è un numero intero a 32 bit perché l'aritmetica a 32 bit è complessivamente più veloce su x86 rispetto all'aritmetica a 16 bit.

Questo è importante perché dovresti essere in grado di utilizzare tipi che possono contenere i risultati dei calcoli eseguiti dal tuo programma indipendentemente dalla piattaforma. Se un programma produce risultati corretti su una piattaforma, ma risultati errati su un altro a causa di differenze nell'overflow di interi, ciò è negativo. Usando i tipi di interi standard, garantisci che i risultati su piattaforme diverse saranno gli stessi per quanto riguarda la dimensione degli interi usati (ovviamente potrebbero esserci altre differenze tra piattaforme oltre alla larghezza dei numeri interi).

Quindi sì, long dovrebbe essere bannato dal codice C ++ moderno. Quindi dovrebbe int , short e long long .

    
risposta data 05.05.2016 - 22:24
fonte
37

No, vietare i tipi interi integrati sarebbe assurdo. Tuttavia, non dovrebbero subire abusi.

Se hai bisogno di un numero intero che sia esattamente N largo, usa std::intN_t (o std::uintN_t se hai bisogno di una versione unsigned ). Pensare a int come numero intero a 32 bit e long long come numero intero a 64 bit è semplicemente sbagliato. Potrebbe succedere di essere così nelle tue attuali piattaforme, ma questo si basa sul comportamento definito dall'implementazione.

L'uso di tipi interi a larghezza fissa è utile anche per interagire con altre tecnologie. Ad esempio, se alcune parti della tua applicazione sono scritte in Java e altre in C ++, probabilmente vorrai abbinare i tipi interi in modo da ottenere risultati coerenti. (Sappi comunque che l'overflow in Java ha semantica ben definita mentre l'overflow signed in C ++ è un comportamento indefinito, quindi la coerenza è un obiettivo elevato.) Inoltre saranno preziosi quando scambieranno dati tra diversi host di calcolo.

Se non hai bisogno esattamente dei bit N , ma solo di un tipo sufficientemente largo , considera l'utilizzo di std::int_leastN_t (ottimizzato per lo spazio) o std::int_fastN_t (ottimizzato per la velocità). Ancora una volta, entrambe le famiglie hanno anche unsigned controparti.

Quindi, quando usare i tipi predefiniti? Bene, dato che lo standard non specifica con precisione la loro larghezza, usali quando non ti preoccupi della larghezza effettiva del bit ma di altre caratteristiche.

Un char è il più piccolo intero indirizzabile dall'hardware. Il linguaggio in realtà ti costringe a usarlo per l'aliasing della memoria arbitraria. È anche l'unico tipo valido per rappresentare stringhe di caratteri (strette).

Un int di solito è il tipo più veloce che la macchina può gestire. Sarà sufficientemente ampio in modo che possa essere caricato e memorizzato con una singola istruzione (senza dover mascherare o spostare i bit) e abbastanza stretto in modo che possa essere utilizzato con (il più) efficiente istruzione hardware. Pertanto, int è una scelta perfetta per il trasferimento di dati e l'esecuzione di operazioni aritmetiche quando l'overflow non è un problema. Ad esempio, il tipo di enumerazione di base predefinito è int . Non cambiarlo in un intero a 32 bit solo perché è possibile. Inoltre, se hai un valore che può essere solo -1, 0 e 1, un int è una scelta perfetta, a meno che non stai per archiviare enormi matrici di essi nel qual caso potresti voler utilizzare dati più compatti digitare al costo di dover pagare un prezzo più alto per l'accesso ai singoli elementi. Un caching più efficiente probabilmente pagherà per questi. Molte funzioni del sistema operativo sono anche definite in termini di int . Sarebbe sciocco convertire i loro argomenti e risultati avanti e indietro. Tutto ciò che potrebbe fare è introdurre errori di overflow.

long di solito è il tipo più largo che può essere gestito con istruzioni su macchina singola. Questo rende particolarmente interessante la percentuale diunsigned long per gestire i dati grezzi e tutti i tipi di manipolazione di bit. Ad esempio, mi aspetto di vedere unsigned long nell'implementazione di un vettore bit. Se il codice è scritto con attenzione, non importa quanto effettivamente sia effettivamente il tipo (perché il codice si adatterà automaticamente). Su piattaforme in cui la parola macchina nativa è a 32 bit, con l'array di supporto del vettore bit è una matrice di unsigned Gli interi a 32 bit sono i più desiderabili perché sarebbe sciocco usare un tipo a 64 bit che deve essere caricato tramite costose istruzioni solo per spostare e mascherare di nuovo i bit non necessari di nuovo. D'altra parte, se la dimensione della parola nativa della piattaforma è di 64 bit, voglio un array di quel tipo perché significa che operazioni come "find first set" possono essere eseguite fino a due volte più velocemente. Quindi il "problema" del tipo di dati long che stai descrivendo, che le sue dimensioni variano da piattaforma a piattaforma, in realtà è una funzione che può essere messa a buon uso. Diventa un problema solo se si pensa ai tipi predefiniti come tipi di una certa larghezza di bit, che semplicemente non lo sono.

char , int e long sono tipi molto utili come descritto sopra. short e long long non sono altrettanto utili perché la loro semantica è molto meno chiara.

    
risposta data 05.05.2016 - 23:44
fonte
6

Un'altra risposta già elabora i tipi di cstdint e le varianti meno conosciute al suo interno.

Vorrei aggiungere a questo:

utilizza nomi di tipi specifici del dominio

Cioè, non dichiarare i tuoi parametri e le tue variabili come uint32_t (certamente non long !), ma nomi come channel_id_type , room_count_type ecc.

sulle librerie

Le librerie di terze parti che usano long o whatnot possono essere fastidiose, specialmente se usate come riferimenti o puntatori a quelle.

La cosa migliore è quella di creare wrapper.

In generale, la mia strategia consiste nel creare una serie di funzioni simili a quelle che verranno utilizzate. Sono sovraccaricati per accettare solo quei tipi che corrispondono esattamente ai tipi corrispondenti, insieme a qualsiasi puntatore, ecc. Variazioni di cui hai bisogno. Sono definiti specifici per l'os / il compilatore / le impostazioni. Ciò ti consente di rimuovere gli avvisi e tuttavia di garantire che vengano utilizzate solo le conversioni "giuste".

channel_id_type cid_out;
...
SomeLibFoo (same_thing_really<int*>(&cid_out));

In particolare, con diversi tipi primitivi che producono 32 bit, la scelta di come int32_t è definita potrebbe non corrispondere alla chiamata della libreria (ad es. int vs long su Windows).

La funzione simile al cast documents the clash, fornisce il controllo in fase di compilazione sul risultato che corrisponde al parametro della funzione, e rimuove qualsiasi avviso o errore se e solo se il tipo effettivo corrisponde alla dimensione reale coinvolta. Cioè, è sovraccarico e definito se passo (su Windows) un int* o un long* e restituisce altrimenti un errore in fase di compilazione.

Quindi, se la libreria è aggiornata o qualcuno cambia ciò che channel_id_type è, questo continua a essere verificato.

    
risposta data 07.05.2016 - 03:30
fonte

Leggi altre domande sui tag