Warum sollte ich für eingebetteten Code "uint_t" -Typen anstelle von "unsigned int" verwenden?

22

Ich schreibe eine Anwendung in c für einen STM32F105 mit gcc.

In der Vergangenheit (mit einfacheren Projekten), habe ich immer Variablen wie definiert char, int, unsigned int, und so weiter.

Ich sehe , dass es üblich ist , die Typen in stdint.h, wie definiert zu verwenden int8_t, uint8_t, uint32_tusw. Dies freilich in mehreren APIs , dass ich verwende, und auch in der ARM CMSIS Bibliothek von ST.

Ich glaube, ich verstehe, warum wir das tun sollten. um es dem Compiler zu ermöglichen, den Speicherplatz besser zu optimieren. Ich gehe davon aus, dass es weitere Gründe geben kann.

Aufgrund der Regeln für die Heraufstufung von Ganzzahlen in c stoße ich jedoch immer wieder auf Konvertierungswarnungen, wenn ich versuche, zwei Werte hinzuzufügen, eine bitweise Operation durchzuführen usw. Die Warnung lautet etwa conversion to 'uint16_t' from 'int' may alter its value [-Wconversion]. Das Thema wird hier und hier diskutiert .

Dies passiert nicht, wenn Variablen verwendet werden, die als intoder deklariert sind unsigned int.

Um einige Beispiele zu nennen:

uint16_t value16;
uint8_t value8;

Das müsste ich ändern:

value16 <<= 8;
value8 += 2;

dazu:

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

Es ist hässlich, aber ich kann es tun, wenn nötig. Hier sind meine Fragen:

  1. Gibt es einen Fall, in dem die Konvertierung von nicht signiert zu signiert und zurück zu nicht signiert das Ergebnis falsch macht?

  2. Gibt es andere wichtige Gründe für / gegen die Verwendung der Integer-Typen stdint.h?

Basierend auf den Antworten, die ich erhalte, sieht es so aus, als ob die stdint.h-Typen im Allgemeinen bevorzugt werden, obwohl c uintnach intund zurück konvertiert wird . Dies führt zu einer größeren Frage:

  1. Ich kann die Compiler-Warnungen durch Typecasting (zB value16 = (uint16_t)(value16 << 8);) verhindern. Verstecke ich nur das Problem? Gibt es einen besseren Weg, dies zu tun?
bitsmack
quelle
Verwenden Sie vorzeichenlose Literale: dh 8u und 2u.
Hören Sie auf, Monica
Danke, @OrangeDog, ich glaube, ich habe ein Missverständnis. Ich habe beides versuchtvalue8 += 2u; undvalue8 = value8 + 2u; , aber ich bekomme die gleichen Warnungen.
Bitsmack
Verwenden Sie sie trotzdem, um signierte Warnungen zu vermeiden, wenn Sie noch keine
Breitenwarnungen

Antworten:

11

Ein normenkonformer Compiler mit int17 bis 32 Bit kann mit folgendem Code legitimerweise alles tun, was er will :

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

Eine Implementierung, die dies wollte, konnte zu Recht ein Programm generieren, das nichts anderes tun würde, als die Zeichenfolge "Fred" wiederholt auf jedem Port-Pin unter Verwendung jedes erdenklichen Protokolls auszugeben. Die Wahrscheinlichkeit, dass ein Programm auf eine Implementierung portiert wird, die so etwas tun würde, ist außergewöhnlich gering, aber theoretisch möglich. Wenn der obige Code so geschrieben werden soll, dass garantiert kein undefiniertes Verhalten auftritt, muss der letztere Ausdruck als (uint32_t)x*xoder geschrieben werden 1u*x*x. Auf einem Compiler mit int17 bis 31 Bits würde der letztgenannte Ausdruck die oberen Bits entfernen, sich jedoch nicht auf Undefiniertes Verhalten einlassen.

Ich denke, dass die gcc-Warnungen wahrscheinlich darauf hindeuten, dass der geschriebene Code nicht zu 100% portierbar ist. Es gibt Zeiten, in denen Code wirklich geschrieben werden sollte, um Verhaltensweisen zu vermeiden, die bei einigen Implementierungen undefiniert wären. In vielen anderen Fällen sollte man jedoch einfach herausfinden, dass der Code bei Implementierungen, die übermäßig lästige Dinge bewirken, wahrscheinlich nicht verwendet wird.

Beachten Sie, dass die Verwendung von Typen wie intund shorteinige Warnungen beseitigen und einige Probleme beheben kann, wahrscheinlich jedoch andere verursacht. Die Interaktion zwischen Typen wie uint16_tund Cs Regeln zur Heraufstufung von Ganzzahlen ist icky, aber solche Typen sind wahrscheinlich immer noch besser als jede Alternative.

Superkatze
quelle
6

1) Wenn Sie nur von einer vorzeichenlosen zu einer vorzeichenbehafteten Ganzzahl gleicher Länge hin und her wandeln, ohne dass dazwischen Operationen ausgeführt werden müssen, erhalten Sie jedes Mal das gleiche Ergebnis, also hier kein Problem. Verschiedene logische und arithmetische Operationen wirken sich jedoch auf vorzeichenbehaftete und vorzeichenlose Operanden unterschiedlich aus.
2) Der Hauptgrund zur Verwendung stdint.hArten ist , dass die Bit - Größe eines solchen Typen definiert sind und gleiche über alle Plattformen, die nicht wahr ist int, longetc, sowie charhat keinen Standard signess, kann es mit oder ohne Vorzeichen werden durch Standard. Es erleichtert die Manipulation der Daten, wenn die genaue Größe bekannt ist, ohne dass zusätzliche Überprüfungen und Annahmen erforderlich sind.

Eugene Sh.
quelle
2
Die Größen von int32_tund uint32_tsind auf allen Plattformen, auf denen sie definiert sind, gleich . Wenn der Prozessor keinen genau passenden Hardwaretyp hat, sind diese Typen nicht definiert. Daher der Vorteil von intusw. und vielleicht int_least32_tusw.
Pete Becker
1
@PeteBecker - Das ist aber wohl ein Vorteil, denn die daraus resultierenden Kompilierungsfehler machen Sie sofort auf das Problem aufmerksam. Das wäre mir viel lieber, als dass meine Typen meine Größe ändern.
Sapi
@sapi - In vielen Situationen spielt die zugrunde liegende Größe keine Rolle. C-Programmierer kamen über viele Jahre hinweg ohne feste Größen zurecht.
Pete Becker
6

Da Eugenes Nr. 2 wahrscheinlich der wichtigste Punkt ist, möchte ich nur hinzufügen, dass es ein Hinweis ist

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

Auch Jack Ganssle scheint ein Befürworter dieser Regel zu sein: http://www.ganssle.com/tem/tem265.html

Tom L.
quelle
2
Es ist schade, dass es keine Typen für die Angabe von "N-Bit-Ganzzahlen ohne Vorzeichen, die sicher mit jeder anderen Ganzzahl gleicher Größe multipliziert werden können, um das Ergebnis gleicher Größe zu erhalten" gibt. Integer-Promotion-Regeln interagieren schrecklich mit den vorhandenen Typen wie uint32_t.
Supercat
3

Eine einfache Möglichkeit, die Warnungen zu beseitigen, besteht darin, die Verwendung von -Wconversion in GCC zu vermeiden. Ich denke, Sie müssen diese Option manuell aktivieren, aber wenn nicht, können Sie -Wno-Konvertierung verwenden, um es zu deaktivieren. Sie können Warnungen für Vorzeichen- und FP-Präzisionskonvertierungen über andere Optionen aktivieren , wenn Sie diese weiterhin benötigen.

Die -Wandlungswarnungen sind fast immer falsch positiv, weshalb wahrscheinlich nicht einmal -Wextra sie standardmäßig aktiviert. Eine Stapelüberlauf-Frage enthält viele Vorschläge für gute Optionssätze. Nach meinen eigenen Erfahrungen ist dies ein guter Anfang:

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

Fügen Sie mehr hinzu, wenn Sie sie brauchen, aber wahrscheinlich nicht.

Wenn Sie -Wconversion beibehalten müssen, können Sie den Code ein wenig verkürzen, indem Sie nur den numerischen Operanden tippen:

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

Das ist jedoch ohne Hervorhebung der Syntax nicht einfach zu lesen.

Adam Haun
quelle
2

In jedem Softwareprojekt ist es sehr wichtig, tragbare Typdefinitionen zu verwenden. (Auch die nächste Version desselben Compilers benötigt diese Überlegung.) Ein gutes Beispiel: Vor einigen Jahren habe ich ein Projekt bearbeitet, in dem der aktuelle Compiler 'int' als 8 Bit definiert hat. In der nächsten Version des Compilers wurde 'int' als 16 Bit definiert. Da wir keine portablen Definitionen für 'int' verwendet hatten, verdoppelte sich die Größe des RAM (effektiv), und viele Codesequenzen, die von einem 8-Bit-Int abhängig waren, scheiterten. Die Verwendung einer portablen Typdefinition hätte dieses Problem (Hunderte von Arbeitsstunden) vermieden.

Richard Williams
quelle
Es sollte kein angemessener Code verwendet werden int, um auf einen 8-Bit-Typ zu verweisen. Selbst wenn ein Nicht-C-Compiler wie CCS dies tut, sollte angemessener Code entweder chareinen typisierten Typ für 8 Bit und einen typisierten Typ (nicht "lang") für 16 Bit verwenden. Auf der anderen Seite ist das Portieren von Code von so etwas wie CCS auf einen echten Compiler problematisch, selbst wenn geeignete Typedefs verwendet werden, da solche Compiler auf andere Weise häufig "ungewöhnlich" sind.
Supercat
1
  1. Ja. Eine Ganzzahl mit n-Bit-Vorzeichen kann ungefähr die Hälfte der Anzahl nicht negativer Zahlen als Ganzzahl ohne n-Bit-Vorzeichen darstellen. Wenn Sie sich auf Überlaufmerkmale verlassen, ist dies undefiniertes Verhalten, sodass alles passieren kann. Die überwiegende Mehrheit der aktuellen und früheren Prozessoren verwendet Zweierkomplemente, sodass viele Operationen für vorzeichenbehaftete und vorzeichenlose Integraltypen dasselbe tun, aber selbst dann führen nicht alle Operationen zu bitweise identischen Ergebnissen. Sie werden später wirklich um zusätzliche Probleme bitten, wenn Sie nicht herausfinden können, warum Ihr Code nicht wie beabsichtigt funktioniert.

  2. Während int und unsigned implementierungsdefinierte Größen haben, werden diese häufig von der Implementierung aus Gründen der Größe oder Geschwindigkeit "intelligent" ausgewählt. Ich halte mich im Allgemeinen an diese, es sei denn, ich habe einen guten Grund, etwas anderes zu tun. Wenn ich überlege, ob ich int oder unsigned verwenden soll, bevorzuge ich im Allgemeinen int, es sei denn, ich habe einen guten Grund, dies anders zu tun.

In Fällen, in denen ich wirklich eine bessere Kontrolle über die Größe oder Signiertheit eines Typs benötige, bevorzuge ich normalerweise die Verwendung eines systemdefinierten typedef (size_t, intmax_t usw.) oder erstelle einen eigenen typedef, der die Funktion eines bestimmten Typs angibt Typ (prng_int, adc_int usw.).

helloworld922
quelle
0

Oft wird der Code auf ARM Thumb und AVR (und x86, PowerPC und anderen Architekturen) verwendet, und 16 oder 32 Bit können auf STM32 ARM effizienter sein (in beide Richtungen: Flash und Zyklen), selbst für eine Variable, die in 8 Bit passt ( 8 Bit ist bei AVR effizienter) . Wenn der SRAM jedoch fast voll ist, kann das Zurücksetzen auf 8 Bit für globale Variablen sinnvoll sein (nicht jedoch für lokale Variablen). Für die Portabilität und Wartung (insbesondere für 8-Bit-Versionen) hat es Vorteile (ohne Nachteile) , die MINIMUM-geeignete Größe anstelle der exakten Größe und der Typendefinition an einer Stelle (normalerweise unter ifdef) anzugeben , um die Einstellung vorzunehmen (möglicherweise uint_fast8_t /). uint_least8_t) während der Portierung / Build-Zeit, zB:

// 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)

Die GNU-Bibliothek hilft ein bisschen, aber normalerweise sind die typedefs trotzdem sinnvoll:

typedef uint_least8_t uintG8_t;
typedef uint_fast8_t uintL8_t;

// aber uint_fast8_t für BEIDE, wenn SRAM kein Problem ist.

Marcell
quelle