Casting doesn't work as expected

I have found that casting doesn't work as expected.

Simple example:
uint8_t a=201, b=200;
Serial.println( a-b ); //corect +1
Serial.println( b-a ); //incorect -1
Serial.println( (uint8_t)b-a ); //incorrect -1
Serial.println( (b-a)&255 ); //correct 255

uint8_t c=b-a;
Serial.println( c ); //correct 255

How on the world the result is -1 as all variables are UNSIGNED 8 bit?!?

Serial.println( (uint8_t)b-a ); //incorrect -1

don't you want to cast the result, not just b?

  `Serial.println( (uint8_t) (b-a) );`

i get the following, when i do

1
-1
255
255
255

didn't expect 16 bit result as all other variables are 8 bit.

why do you think it's a 16 bit result?

that's because of a misunderstanding of the C++ specification

Integral promotion

prvalues of small integral types (such as char) may be converted to prvalues of larger integral types (such as int). In particular, arithmetic operators do not accept types smaller than int as arguments, and integral promotions are automatically applied after lvalue-to-rvalue conversion, if applicable. This conversion always preserves the value.

The following implicit conversions are classified as integral promotions:

  • signed char or signed short can be converted to int;
  • unsigned char , char8_t (since C++20) or unsigned short can be converted to int if it can hold its entire value range, and unsigned int otherwise;

What happens in your specific case is not due to casting, or at least not the way you think about it.
It's the way the result of the operation (unless you specifically cast the result before calling the print method) goes though Integral promotion and thus the the compiler picks the print() method with the integer signature.

Try this code where I've defined a few whatsMyType() functions signatures.

uint8_t a = 201;
uint8_t b = 200;

void whatsMyType(uint8_t v)
{
  Serial.print(v);
  Serial.println(F(" is of type uint8_t"));
}

void whatsMyType(int8_t v)
{
  Serial.print(v);
  Serial.println(F(" is of type int8_t"));
}

void whatsMyType(int v)
{
  Serial.print(v);
  Serial.println(F(" is of type int"));
}

void setup() {
  Serial.begin(115200);
  whatsMyType( a - b ); //corect +1
  whatsMyType( b - a ); //incorect -1
  whatsMyType( (uint8_t) b - a ); //incorrect -1
  whatsMyType( (b - a) & 255 ); //correct 255

  uint8_t c = b - a;
  whatsMyType( c ); //correct 255
}

void loop() {}

Serial Monitor (@ 115200 bauds) will show

1 is of type int
-1 is of type int
-1 is of type int
255 is of type int
255 is of type uint8_t

Now pick a larger type for a and b to not fall in the case of

In particular, arithmetic operators do not accept types smaller than int as arguments

and run this code:

uint16_t a = 201;
uint16_t b = 200;

void whatsMyType(uint8_t v)
{
  Serial.print(v);
  Serial.println(F(" is of type uint8_t"));
}

void whatsMyType(uint16_t v)
{
  Serial.print(v);
  Serial.println(F(" is of type uint16_t"));
}

void whatsMyType(int16_t v)
{
  Serial.print(v);
  Serial.println(F(" is of type int16_t"));
}

void setup() {
  Serial.begin(115200);
  whatsMyType( a - b ); //corect +1
  whatsMyType( b - a ); //incorect -1
  whatsMyType( (uint8_t) b - a ); //incorrect -1
  whatsMyType( (b - a) & 255 ); //correct 255

  uint8_t c = b - a;
  whatsMyType( c ); //correct 255
}

void loop() {}

the result will be more in line with what you would have expected:

Serial Monitor (@ 115200 bauds) will show
1 is of type uint16_t
65535 is of type uint16_t
65535 is of type uint16_t
255 is of type uint16_t
255 is of type uint8_t

Bjarne Stroustrup, the designer of C++, supposedly said,

Using an unsigned instead of an int to gain one more bit to represent positive integers is almost never a good idea.

Thanks for explanation. Now I know that I need extra care when using 8 bit variable. That cost me near a week to find the problem as problem cause trouble in 1/128 cases.

I think it is a bit more complex yet. When the compiler picks the best match on the Serial.print() argument types there may be an unexpected conversion. The casting is for the phrase in which it resides and is not necessarily carried into future statements.

This is true in general if there is no directly matching type and there is a valid promotion possible to a larger compatible type

But in this specific case I believe it’s the byte nature of the data and promotion to int in the arithmetic operation that is the root cause, as evidenced by the second code where I used uint16_t
(And there are signed and unsigned char version of the print method so if the type had been on one byte, that’s the method which would have been picked)