Per il codice incorporato, perché dovrei usare i tipi "uint_t" invece di "unsigned int"?


22

Sto scrivendo un'applicazione in c per un STM32F105, usando gcc.

In passato (con i progetti più semplici), ho sempre variabili definite come char, int, unsigned inte così via.

Vedo che è comune utilizzare i tipi definiti in stdint.h, come int8_t, uint8_t, uint32_t, ecc Questo è vero in API multipla che sto usando, e anche nella biblioteca ARM CMSIS da ST.

Credo di capire perché dovremmo farlo; per consentire al compilatore di ottimizzare meglio lo spazio di memoria. Mi aspetto che possano esserci ulteriori motivi.

Tuttavia, a causa delle regole di promozione dei numeri interi di c, continuo ad imbattermi in avvisi di conversione ogni volta che provo ad aggiungere due valori, eseguire un'operazione bit a bit, ecc. L'avviso legge qualcosa di simile conversion to 'uint16_t' from 'int' may alter its value [-Wconversion]. Il problema è discusso qui e qui .

Non succede quando si usano variabili dichiarate come into unsigned int.

Per fare un paio di esempi, dato questo:

uint16_t value16;
uint8_t value8;

Dovrei cambiare questo:

value16 <<= 8;
value8 += 2;

a questa:

value16 = (uint16_t)(value16 << 8);
value8 = (uint8_t)(value8 + 2);

È brutto, ma posso farlo se necessario. Ecco le mie domande:

  1. Esiste un caso in cui la conversione da non firmato a firmato e viceversa a non firmato renderà il risultato errato?

  2. Ci sono altri grandi motivi per / contro l'uso dei tipi interi stdint.h?

Sulla base delle risposte che sto ricevendo, sembra che i tipi stdint.h siano generalmente preferiti, anche se c converte uintin inte viceversa . Questo porta a una domanda più grande:

  1. Posso evitare gli avvisi del compilatore usando la tipografia (ad es value16 = (uint16_t)(value16 << 8);.). Sto solo nascondendo il problema? C'è un modo migliore per farlo?

Usa letterali non firmati: ie 8ue 2u.
Smetti di fare del male a Monica il

Grazie, @OrangeDog, penso di essere frainteso. Ho provato entrambi value8 += 2u;e value8 = value8 + 2u;, ma ricevo gli stessi avvertimenti.
bitsmack,

Usali comunque, per evitare avvisi firmati quando non hai già avvisi di larghezza :)
Smetti di danneggiare Monica il

Risposte:


11

Un compilatore conforme agli standard in cui intera compreso tra 17 e 32 bit può legittimamente fare tutto ciò che vuole con il seguente codice:

uint16_t x = 46341;
uint32_t y = x*x; // temp result is signed int, which can't hold 2147488281

Un'implementazione che voleva farlo poteva legittimamente generare un programma che non avrebbe fatto altro che emettere ripetutamente la stringa "Fred" su ogni pin della porta usando ogni protocollo immaginabile. La probabilità che un programma venga portato su un'implementazione che farebbe una cosa del genere è eccezionalmente bassa, ma è teoricamente possibile. Se si desidera scrivere il codice sopra riportato in modo da garantire che non si impegni in un comportamento indefinito, sarebbe necessario scrivere quest'ultima espressione come (uint32_t)x*xo 1u*x*x. Su un compilatore con valori intcompresi tra 17 e 31 bit, quest'ultima espressione eliminerebbe i bit superiori, ma non si impegnerebbe in un comportamento indefinito.

Penso che gli avvisi di gcc stiano probabilmente provando a suggerire che il codice scritto non è completamente portatile al 100%. Ci sono momenti in cui il codice dovrebbe davvero essere scritto per evitare comportamenti che sarebbero indefiniti in alcune implementazioni, ma in molti altri casi si dovrebbe semplicemente immaginare che è improbabile che il codice venga utilizzato su implementazioni che farebbero cose eccessivamente fastidiose.

Si noti che l'utilizzo di tipi come inte shortpuò eliminare alcuni avvisi e risolvere alcuni problemi, ma probabilmente ne creerebbe altri. L'interazione tra tipi come uint16_te le regole di promozione dei numeri interi è pericolosa, ma tali tipi sono probabilmente ancora migliori di qualsiasi alternativa.


6

1) Se esegui il cast da numeri interi senza segno a quelli con la stessa lunghezza avanti e indietro, senza alcuna operazione intermedia, otterrai lo stesso risultato ogni volta, quindi nessun problema qui. Ma varie operazioni logiche e aritmetiche agiscono in modo diverso su operandi con o senza segno.
2) Il motivo principale per utilizzare i stdint.htipi è che le dimensioni dei bit di tali tipi sono definite e uguali su tutte le piattaforme, il che non è vero per int, longecc., Così come charnon ha un segno standard, può essere firmato o non firmato da predefinito. Semplifica la manipolazione dei dati conoscendo la dimensione esatta senza utilizzare ulteriori controlli e ipotesi.


2
Le dimensioni di int32_te uint32_tsono uguali su tutta la piattaforma su cui sono definite . Se il processore non ha un tipo di hardware esattamente corrispondente , questi tipi non sono definiti. Da qui il vantaggio di intecc. E, forse, int_least32_tecc.
Pete Becker,

1
@PeteBecker - Questo è probabilmente un vantaggio, perché gli errori di compilazione risultanti ti rendono immediatamente consapevole del problema. Preferirei molto di più che i miei tipi cambino dimensione su di me.
sapi,

@sapi - in molte situazioni la dimensione sottostante è irrilevante; I programmatori C andarono d'accordo per anni senza dimensioni fisse.
Pete Becker,

6

Poiché il numero 2 di Eugene è probabilmente il punto più importante, vorrei solo aggiungere che è un avviso in

MISRA (directive 4.6): "typedefs that indicate size and signedness should be used in place of the basic types".

Anche Jack Ganssle sembra essere un sostenitore di questa regola: http://www.ganssle.com/tem/tem265.html


2
Peccato che non ci siano tipi per specificare "Numero intero senza segno N-bit che può essere tranquillamente moltiplicato per qualsiasi altro numero intero della stessa dimensione per ottenere lo stesso risultato". Le regole di promozione intere interagiscono in modo orribile con i tipi esistenti come uint32_t.
supercat

3

Un modo semplice per eliminare gli avvisi è quello di evitare di usare -Wconversion in GCC. Penso che devi abilitare questa opzione manualmente, ma in caso contrario, puoi usare -Wno-conversione per disabilitarlo. Puoi abilitare gli avvisi per le conversioni di precisione dei segni e FP tramite altre opzioni , se desideri ancora.

Gli avvertimenti di -Wconversion sono quasi sempre falsi positivi, motivo per cui nemmeno -Wextra lo abilita per impostazione predefinita. Una domanda Stack Overflow ha molti suggerimenti per buoni set di opzioni. Sulla base della mia esperienza, questo è un buon punto di partenza:

-std = c99 -pedantic -Wall -Wextra -Wshadow

Aggiungi altro se ne hai bisogno, ma le probabilità sono che non lo farai.

Se devi mantenere -Wconversion, puoi accorciare un po 'il codice digitando solo l'operando numerico:

value16 <<= (uint16_t)8;
value8 += (uint8_t)2;

Tuttavia, non è facile da leggere senza l'evidenziazione della sintassi.


2

in qualsiasi progetto software, è molto importante utilizzare definizioni di tipo portatili. (anche la prossima versione dello stesso compilatore richiede questa considerazione.) Un buon esempio, diversi anni fa ho lavorato a un progetto in cui l'attuale compilatore ha definito "int" come 8 bit. La versione successiva del compilatore ha definito 'int' come 16 bit. Dato che non avevamo usato alcuna definizione portatile per "int", la ram (effettivamente) raddoppiò in dimensioni e molte sequenze di codice che dipendevano da un int a 8 bit fallirono. L'uso di una definizione di tipo portatile avrebbe evitato quel problema (centinaia di ore uomo per risolvere).


Nessun codice ragionevole dovrebbe usare intper fare riferimento a un tipo a 8 bit. Anche se un compilatore non C come CCS lo fa, un codice ragionevole dovrebbe usare uno charo un tipo typedefed per 8 bit e un tipo typedefed (non "long") per 16 bit. D'altra parte, il porting del codice da qualcosa come CCS su un vero compilatore può essere problematico anche se utilizza i typedef corretti, dal momento che tali compilatori sono spesso "insoliti" in altri modi.
supercat il

1
  1. Sì. Un numero intero con segno n-bit può rappresentare circa la metà del numero di numeri non negativi come numero intero senza segno n-bit e fare affidamento sulle caratteristiche di overflow è un comportamento indefinito, quindi può succedere di tutto. La stragrande maggioranza dei processori attuali e passati usa due complementi, quindi molte operazioni fanno la stessa cosa su tipi integrali con o senza segno, ma anche in questo caso non tutte le operazioni produrranno risultati identici a livello di bit. In seguito chiederai davvero ulteriori problemi quando non riesci a capire perché il tuo codice non funziona come previsto.

  2. Mentre int e unsigned hanno dimensioni definite dall'implementazione, queste vengono spesso scelte "in modo intelligente" dall'implementazione per ragioni di dimensioni o velocità. In genere mi attengo a questi a meno che non abbia una buona ragione per fare diversamente. Allo stesso modo, quando si considera se usare int o unsigned preferisco generalmente int a meno che non abbia una buona ragione per farlo diversamente.

Nei casi in cui ho davvero bisogno di un migliore controllo sulla dimensione o sulla firma di un tipo, di solito preferirò utilizzare un typedef definito dal sistema (size_t, intmax_t, ecc.) O creare un mio typedef che indica la funzione di un dato digitare (prng_int, adc_int, ecc.).


0

Spesso il codice viene utilizzato su ARM thumb e AVR (e x86, powerPC e altre architetture) e 16 o 32 bit possono essere più efficienti (entrambi i modi: flash e cicli) su STM32 ARM anche per una variabile che si adatta a 8 bit ( 8 bit è più efficiente su AVR) . Tuttavia, se SRAM è quasi pieno, il ripristino a 8 bit per le variabili globali può essere ragionevole (ma non per le variabili locali). Per la portabilità e la manutenzione (specialmente per i var a 8 bit), ha i vantaggi (senza alcuno svantaggio) di specificare la dimensione MINIMA adatta , anziché la dimensione esatta e digitare in un posto .h (di solito sotto ifdef) per sintonizzarsi (possibilmente uint_fast8_t / uint_least8_t) durante il porting / tempo di costruzione, ad es .:

// apparently uint16_t is just as efficient as 32 bit on STM32, but 8 bit is punished (with more flash and cycles)
typedef uint16_t uintG8_t; // 8bit if SRAM is scarce (use fol global vars that fit in 8 bit)
typedef uint16_t uintL8_t; // 8bit on AVR (local var, 16 or 32 bit is more efficient on STM + less flash)
// might better reserve 32 bits on some arch, STM32 seems efficient with 16 bits:
typedef uint16_t uintG16_t; // 16bit if SRAM is scarce (use fol global vars that fit in 16 bit)
typedef uint16_t uintL16_t; // 16bit on AVR (local var, 16 or 32 bit whichever is more efficient on other arch)

La libreria GNU aiuta un po ', ma normalmente i typedef hanno comunque senso:

typedef uint_least8_t uintG8_t;
typedef uint_fast8_t uintL8_t;

// ma uint_fast8_t per ENTRAMBI quando SRAM non è un problema.

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.