24
\$\begingroup\$

I am writing an application in c for an STM32F105, using gcc.

In the past (with simpler projects), I have always defined variables as char, int, unsigned int, and so on.

I see that it is common to use the types defined in stdint.h, such as int8_t, uint8_t, uint32_t, etc. This it true in multiple API's that I am using, and also in the ARM CMSIS library from ST.

I believe that I understand why we should do so; to allow the compiler to better optimize memory space. I expect there may be additional reasons.

However, because of c's integer promotion rules, I keep running up against conversion warnings any time I try to add two values, do a bitwise operation, etc. The warning reads something like conversion to 'uint16_t' from 'int' may alter its value [-Wconversion]. The issue is discussed here and here.

It doesn't happen when using variables declared as int or unsigned int.

To give a couple of examples, given this:

uint16_t value16;
uint8_t value8;

I would have to change this:

value16 <<= 8;
value8 += 2;

to this:

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

It's ugly, but I can do it if necessary. Here are my questions:

  1. Is there a case where the conversion from unsigned to signed and back to unsigned will make the result incorrect?

  2. Are there any other big reasons for/against using the stdint.h integer types?

Based on the answers I'm receiving, it looks like the stdint.h types are generally preferred, even though c converts uint to int and back. This leads to a bigger question:

  1. I can prevent the compiler warnings by using typecasting (e.g. value16 = (uint16_t)(value16 << 8);). Am I just hiding the problem? Is there a better way to go about it?
\$\endgroup\$
3
  • \$\begingroup\$ Use unsigned literals: i.e. 8u and 2u. \$\endgroup\$
    – OrangeDog
    Commented Dec 23, 2014 at 13:48
  • \$\begingroup\$ Thanks, @OrangeDog, I think I'm misunderstanding. I tried both value8 += 2u; and value8 = value8 + 2u;, but I get the same warnings. \$\endgroup\$
    – bitsmack
    Commented Dec 23, 2014 at 17:27
  • \$\begingroup\$ Use them anyway, to avoid signed warnings when you don't already have width warnings :) \$\endgroup\$
    – OrangeDog
    Commented Dec 23, 2014 at 17:36

7 Answers 7

12
\$\begingroup\$

A standards-conforming compiler where int was anywhere from 17 to 32 bits may legitimately do anything it wants with the following code:

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

An implementation that wanted to do so could legitimately generate a program that would do nothing except output the string "Fred" repeatedly on every port pin using every imaginable protocol. The probability of a program getting ported to an implementation which would do such a thing is exceptionally low, but it is theoretically possible. If want wanted to write the above code so that it would be guaranteed not to engage in Undefined Behavior, it would be necessary write the latter expression as (uint32_t)x*x or 1u*x*x. On a compiler where int is between 17 and 31 bits, the latter expression would lop off the upper bits, but would not engage in Undefined Behavior.

I think the gcc warnings are probably trying to suggest that the code as written is not completely 100% portable. There are times when code really should be written to avoid behaviors which would be Undefined on some implementations, but in many other cases one should simply figure that the code is unlikely to get used on implementations which would do overly annoying things.

Note that using types like int and short may eliminate some warnings and fix some problems, but would likely create others. The interaction between types like uint16_t and C's integer-promotion rules are icky, but such types are still probably better than any alternative.

\$\endgroup\$
8
  • \$\begingroup\$ Just to be clear: uint32_t y = (uint32_t)x*x is what one "should" write here, to standards-objectively express the intuitively-intended outcome of the technically "undefined" code? \$\endgroup\$ Commented May 11, 2020 at 21:11
  • \$\begingroup\$ @JamesTheAwesomeDude: If x is known to be uint16_t and/or hold a value less than sqrt(INT_MAX), that would work, but a conforming implementation could have a short type with 31 value bits, one sign bit, and no padding, unsigned short with 32 value bits and no padding, and signed int with 32 value bits, 1 sign bit, and 7 padding bits. On such an implementation, uint32_t would be a synonym for unsigned short, and so multiplying two uint32_t values, or even incrementing a uint32_t value that was equal to 0xFFFFFFFF could yield Undefined Behavior. \$\endgroup\$
    – supercat
    Commented May 11, 2020 at 21:31
  • \$\begingroup\$ @JamesTheAwesomeDude: When the Standard was written, I would suspect such an implementation would have been seen less implausible than one which targets a common quiet-wraparound 32-bit two's-complement platform but would process unsigned y = x*x; nonsensically for x greater than 46340. \$\endgroup\$
    – supercat
    Commented May 11, 2020 at 21:36
  • 1
    \$\begingroup\$ A uint32_t is capable of losslessly storing the product of two uint16_ts. At least, 0x0000*0x0000=0x00000000, and, at most, 0xFFFF*0xFFFF=0xFFFE0001. If a "naive" developer writes the product of two uint16_ts to be captured as a uint32_t, I don't see any ambiguity about the intent…but how is that intent supposed to be expressed, to "make" a conformant compiler enact it? \$\endgroup\$ Commented May 12, 2020 at 18:30
  • 1
    \$\begingroup\$ @JamesTheAwesomeDude: Casting to the destination container type should work if one isn't interested in mod-reducing the result. If one were performing a hash computation that required multiplying two 32-bit numbers to yield the bottom 32 bits of the result, casting to uint32_t might not cause prevent the computation from being promoted to e.g. a 64-bit signed int. As for the Standard being "odd around the edges", there would be nothing odd about it if compiler writers recognized and honored its intended purpose. \$\endgroup\$
    – supercat
    Commented May 12, 2020 at 20:41
7
\$\begingroup\$

Since Eugene's #2 is probably the most important point, I just would like to add that it's an advisory in

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

Also Jack Ganssle appears to be a supporter of that rule: http://www.ganssle.com/tem/tem265.html

\$\endgroup\$
1
  • 3
    \$\begingroup\$ It's too bad there aren't any types for specify "N-bit unsigned integer which may be safely multiplied by any other same-size integer to yield the same sized result". Integer promotion rules interact horribly with the existing types like uint32_t. \$\endgroup\$
    – supercat
    Commented Dec 22, 2014 at 23:06
6
\$\begingroup\$

1) If you just cast from unsigned to signed integer of the same length back and forth, without any operations in between, you will get the same result every time, so no problem here. But various logical and arithmetical operations are acting differently on signed and unsigned operands.
2) The main reason to use stdint.h types is that the bit size of such a types are defined and equal across all of the platforms, which is not true for int, long e.t.c., as well as char has no standard signess, it can be signed or unsigned by default. It makes easier to manipulate the data knowing the exact size without using extra checking and assumptions.

\$\endgroup\$
4
  • 2
    \$\begingroup\$ The sizes of int32_t and uint32_t are equal across all platform on which they are defined. If the processor doesn't have an exactly matching hardware type, these types are not defined. Hence the advantage of int etc. and, perhaps, int_least32_t etc. \$\endgroup\$ Commented Dec 22, 2014 at 22:14
  • 2
    \$\begingroup\$ @PeteBecker - That's arguably an advantage, though, because the resulting compile errors make you immediately aware of the problem. I'd much rather that than my types changing size on me. \$\endgroup\$
    – sapi
    Commented Dec 23, 2014 at 7:47
  • \$\begingroup\$ @sapi - in many situations the underlying size is irrelevant; C programmers got along just fine without fixed sizes for many years. \$\endgroup\$ Commented Dec 23, 2014 at 13:40
  • \$\begingroup\$ @PeteBecker: Part of the reason programmers got by without "officially" fixed-sized types is that it was commonplace for implementations to treat each type other than int and unsigned int as the smallest type with the required range. While "short" and "long" weren't officially specified as 16-bit and 32-bit types, there was no perceived reason for implementations targeting octet-based platforms to treat them as anything else. \$\endgroup\$
    – supercat
    Commented May 11, 2020 at 22:18
3
\$\begingroup\$

An easy way to eliminate the warnings is to avoid using -Wconversion in GCC. I think you have to enable this option manually, but if not, you can use -Wno-conversion to disable it. You can enable warnings for sign and FP precision conversions via other options, if you still want those.

The -Wconversion warnings are almost always false positives, which is probably why not even -Wextra enables it by default. A Stack Overflow question has a lot of suggestions for good option sets. Based on my own experience, this is a good place to start:

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

Add more if you need them, but odds are you won't.

If you must keep -Wconversion, you can shorten your code a bit by only typecasting the numeric operand:

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

That's not easy to read without syntax highlighting, though.

\$\endgroup\$
2
\$\begingroup\$

in any software project, it is very important to use portable type definitions. (even the next version of the same compiler needs this consideration. ) A good example, several years ago I worked a project where the current compiler defined 'int' as 8bits. The next version of the compiler defined 'int' as 16bits. Since we had not used any portable definitions for 'int', the ram (effectively) doubled in size and many code sequences that depended on a 8bit int failed. The use of a portable type definition would have avoided that (hundreds of man hours to fix) problem.

\$\endgroup\$
1
  • \$\begingroup\$ No reasonable code should use int to refer to an 8 bit type. Even if a non-C compiler like CCS does so, reasonable code should use either char or a typedefed type for 8 bits, and a typedefed type (not "long") for 16 bits. On the other hand, porting code from something like CCS to a real compiler is apt to be problematic even if it uses proper typedefs, since such compilers are often "unusual" in other ways. \$\endgroup\$
    – supercat
    Commented Dec 23, 2014 at 17:56
1
\$\begingroup\$
  1. Yes. An n-bit signed integer can represent roughly half the number of non-negative numbers as an n-bit unsigned integer, and relying on overflow characteristics is undefined behavior so anything can happen. The vast majority of current and past processors use twos complement so a lot of operations happen to do the same thing on signed and unsigned integral types, but even then not all operations will produce bit-wise identical results. You're really asking for extra trouble later on when you can't figure out why your code isn't working as intended.

  2. While int and unsigned have implementation defined sizes, these are often chosen "smartly" by the implementation either for size or speed reasons. I generally stick to these unless I have a good reason for doing otherwise. Likewise, when considering whether to use int or unsigned I generally prefer int unless I have a good reason to do so otherwise.

In cases where I really need better control over the size or signed-ness of a type, I usually will prefer either using a system-defined typedef (size_t, intmax_t, etc.) or make my own typedef which indicates the function of a given type (prng_int, adc_int, etc.).

\$\endgroup\$
0
\$\begingroup\$

Often the code is used on ARM thumb and AVR (and x86, powerPC and other architectures), and 16 or 32 bit can be more efficient (both ways: flash and cycles) on STM32 ARM even for a variable that fits in 8 bits (8 bit is more efficient on AVR). However if SRAM is almost full, reverting to 8 bit for global vars can be reasonable (but not for local vars). For portability and maintenance (especially for 8 bit vars), it has advantages (without any disadvantages) to specify the MINIMUM suitable size, instead of the exact size and typedef at one .h place (usually under ifdef) to tune (possibly uint_fast8_t / uint_least8_t) during porting / build time, eg:

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

GNU library helps a bit, but normally the typedefs make sense anyway:

typedef uint_least8_t uintG8_t;
typedef uint_fast8_t uintL8_t;

// but uint_fast8_t for BOTH when SRAM is not a concern.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.