9

I have the following C code:

typedef unsigned char uint8_t;

void main(void) {
    uint8_t a = 1, b = 2, res;
    res = a + b;
}   

When I compile this code using gcc -Wconversion, I get the following warning:

test.c: In function 'main':
test.c:5:10: warning: conversion to 'uint8_t' from 'int' may alter its value [-Wconversion]

Can someone please explain why this warning appears? All three variables are of type uint8_t, so I don't really understand where the int comes from.

phuclv
  • 37,963
  • 15
  • 156
  • 475
watain
  • 4,838
  • 4
  • 34
  • 35

3 Answers3

12

I don't really understand where the int comes from.

int comes from the C language standard. All operands of arithmetic operators are promoted before performing their operation. In this case uint8_t is promoted to an int, so you need a cast to avoid the warning:

res = (uint8_t)(a + b);

Here is how the standard defines integer promotions:

6.3.1.1 If an int can represent all values of the original type, the value is converted to an int; otherwise, it is converted to an unsigned int. These are called the integer promotions.

Since int can hold all possible values of uint8_t, a and b are promoted to int for the addition operation.

Sergey Kalinichenko
  • 714,442
  • 84
  • 1,110
  • 1,523
  • So every time I do an arithmetic operation I would need to cast it _back_ to `uint8_t`? That's really annoying. Is there a way I can force gcc to not promote it to `int`? – watain Jan 24 '14 at 14:51
  • Thanks for your detailed explanation. So I guess I will just have to cast all back to `uint8_t`. – watain Jan 24 '14 at 14:58
  • 3
    2 other things: 1) `uint8_t` takes more cycles to do math with on most processors. 2) The explicit cast reminds you what the overflow semantics are. If you just add 2 `uint8_t`s, the result can be greater than 255 if you don't cast it back. – nmichaels Jan 24 '14 at 14:58
  • 2
    @watain You cannot force gcc not to promote to int, because that would violate the standard. In reality, the compiler is allowed to optimize the generated code not to promote values to int if it knows that only the last 8 bits are going to be used. However, the code must behave as if the promotion had occurred, including the warning. If you have to cast back to uint8_t a lot, you may want to define a macro, say #define TO_UINT8(x) ((uint8_t)(x)) – Sergey Kalinichenko Jan 24 '14 at 15:02
  • 1
    @JosephQuinsey Absolutely! Thank you very much for reminding me why I hate macros. – Sergey Kalinichenko Jan 24 '14 at 15:02
  • `s/explicit cast/cast/` as casts always use the cast operator and are explicit by definition; whatever could be implicit isn't a cast (probably a promotion or conversion). – Jens Jan 24 '14 at 15:02
  • @nmichaels: Why do you think `uint8_t` arithmetic is slower? Than what, `int` arithmetic? – Eric Postpischil Jan 24 '14 at 15:29
  • 1
    EricPostpischil: Yes, than `int`. It's because the compiler has to emit extra instructions to ensure that the register only holds 8 bits. These get thrown in during arithmetic, after loads, and before stores. Lots of systems won't even let you read a single byte out of memory. Anyway, it varies with processor and matters more in embedded stuff, but it's generally true. That's why C has `uint_fast8_t`. – nmichaels Jan 24 '14 at 15:40
  • 1
    @nmichaels: Arithmetic with `uint_8` generally does not require extra instructions to ensure the register holds only eight bits. One simply does arithmetic normally and ignores the extra bits. This works for addition; subtraction; multiplication; bit-wise ANDs, ORs, XORs, and NOTs; and left-shifts. Division and right-shifts may require more care. And you cannot assume `int` is safe from this; we see current implementations with 32-bit `int` in 64-bit registers. Unless you are targeting a specific machine, it is better to write types that suit your data, rather than to guess at this. – Eric Postpischil Jan 24 '14 at 18:13
  • 1
    @nmichaels: On top of that, converting the code to use `int` does not solve the problem of getting correct results for `uint_8`; it merely moves the burden from the compiler to the programmer, who now has to write correct expressions for their ultimate result using a type they did not want. (E.g., the programmer has to ensure that extra bits are handled before they perform a division.) [Note: To address somebody in Stack Overflow, use “@” before their user ID. Otherwise, they are not notified.] – Eric Postpischil Jan 24 '14 at 18:16
  • @EricPostpischil: The "ignore the extra bits" operation is not free on many platforms. I would never argue that one should use a type that doesn't suit the program; saving a single cycle is almost never worth any effort at all. I was just trying to convey that using 8-bit types is not usually an optimization because I suspected that the OP didn't know it already. Using `uint_fast*_t` does solve the problem but usually that isn't worth the bother either. [thanks, for some reason I thought they had changed that] – nmichaels Jan 26 '14 at 14:17
  • @EricPostpischil: To elaborate, I've seen people try to use `uint8_t` in function formal parameters because they think it's faster and their numbers probably won't overflow 255. That kind of thing stems from the wrong assumption that things will be _faster_ that way. Anyway, this commentary is getting long and I suspect we actually agree so I'll leave it there. – nmichaels Jan 26 '14 at 14:20
4

Just to add to the existing answer about integer promotions, it might also be worth explaining what -Wconversion is warning you about.

Since a and b are both uint8_ts, the result of a + b might not fit in another uint8_t. By assigning the result back into a uint8_t, you force the compiler to perform a conversion which might change the value. Hence, the res variable might not actually represent the actual value of a + b.

For example, if a and b were both 0xff, then:

  • a + b is 0x1fe and has type int
  • (uint8_t)(a + b) is 0xfe
benj
  • 713
  • 6
  • 12
  • But the result of `a + b` might also not fit into an `int` or `unsigned int`. So why does gcc not emit a warning when using `int` or `unsigned int`? – watain Jan 24 '14 at 15:25
  • this isn' true. Doing math on any type can lead to overflow – phuclv Jan 24 '14 at 16:09
  • @watain A uint8_t has a width of exactly 8 bits, but an `int` has to be at least 16 bits wide (in order to satisfy the rules in the standard about the value of `INT_MAX` in ``). Twice the maximum value of an 8-bit number fits in 9 bits - so it'll always fit in an `int` without overflow. – benj Jan 24 '14 at 17:21
0

You should read more about C's int promotion rules. That's the rule, so the compiler must obey. Without it, some "trivial" code won't work (like you expected). For example

char d[4] = {0xFF, 0xFE, 0x80, 40};
int i = (d[0] << 24) | (d[1] << 16) | (d[2] << 8) | d[3];
int j = d[2] - d[0]*d[1];

In some compilers for 8-bit microcontrollers there is an option to disable C standard conformation to speed up maths since always promoting a char to int to do the maths may cause unnecessary operations and slow down the process. It also consumes more memory which is valueable in such embedded systems. Of course it will make some code not portable or not work at all, but that's the trade off.

Modern microprocessors are 16 bit or more, so using char won't save you from any operation but may also increase code size and speed because of the sign adjustment before/after each operations. Most RISC architectures don't even have instruction for operating on operand different from its native size except for load/store and sign extending. So it's recommended to use the native int size for all temporaries. Types smaller than native register size should only be used in case of very large arrays that could increase cache miss, to save or read data from some other types of memory or for compatibility with other systems/libraries

phuclv
  • 37,963
  • 15
  • 156
  • 475