3

The following Arduino (C++) code

void setup()
{
  Serial.begin(115200);
  byte b1 = 12;
  byte b2 = 5;
  const byte RING_BUFFER_SIZE = 64;

  byte diff = b2 - b1;
  byte diff2 = (byte)(b2 - b1) % RING_BUFFER_SIZE; //<---NOTE HOW THE (byte) CAST IS *REQUIRED* TO GET THE RIGHT RESULT!!!!

  Serial.println(b1);
  Serial.println(b2);
  Serial.println(RING_BUFFER_SIZE);
  Serial.println(diff);
  Serial.println(diff2);
}

void loop()
{      
}

produces the expected:

12
5
64
249
57 //<--correct answer

Whereas without the "(byte)" cast as shown here:

void setup()
{
  Serial.begin(115200);
  byte b1 = 12;
  byte b2 = 5;
  const byte RING_BUFFER_SIZE = 64;

  byte diff = b2 - b1;
  byte diff2 = (b2 - b1) % RING_BUFFER_SIZE; //<---(byte) cast removed

  Serial.println(b1);
  Serial.println(b2);
  Serial.println(RING_BUFFER_SIZE);
  Serial.println(diff);
  Serial.println(diff2);
}

void loop()
{      
}

it produces:

12
5
64
249
249 //<--wrong answer

Why the difference? Why does the modulo operator ONLY work with the explicit cast?

Note: "byte" = "uint8_t"

Gabriel Staples
  • 36,492
  • 15
  • 194
  • 265

2 Answers2

4

5 - 12 gives -7 (an int). So your code does -7 % 64.

Mathematically we would expect this to give 57. However, in C and C++, % for negative numbers it doesn't do what you might expect mathematically. Instead it satisfies the following equation:

(a/b) * b + a%b == a

Now, (-7)/64 gives 0 because C and C++ use truncation-towards-zero for integer division of negative-positive. Therefore -7 % 64 evaluates to -7.

Finally, converting -7 to uint8_t gives 249.

When you write (byte)-7 % 64 you are actually doing 249 % 64 giving the expected answer.


Regarding the behaviour of b2 - b1: all integer arithmetic is done in at least int precision; for each operand of -, if it is a narrower integer type than int it is first promoted to int (leaving the value unchanged). Further conversions may occur if the types differ after this promotion (which they don't in this case).

In code, b2 - b1 means (int)b2 - (int)b1 yielding an int; there is no way to specify doing the operation in lower precision.

M.M
  • 138,810
  • 21
  • 208
  • 365
  • 2
    since `b2` and `b1` were both unsigned, I expected a proper "underflow," just like you get overflow to zero if you add 1 to a uint8_t 255. I suppose subtraction rules are different than addition, even when both numbers are *un*signed? – Gabriel Staples Mar 31 '16 at 23:07
  • @GabrielStaples added section explaining that – M.M Mar 31 '16 at 23:20
  • The rationale is perhaps historical: on the first C implementations, `a - b` it would have just loaded the values into word-sized registers and invoked the CPU instruction for subtraction. Instead of jumping through hoops to simulate a lower-precision operation not supported in the hardware. Later standardization then couldn't break programs that relied on this behaviour. – M.M Mar 31 '16 at 23:23
  • 1
    @Gabriel Staples: "Unsignedness" of small types is lost when they get promoted to `signed int`. The only way to make unsigned arithmetic to "strick" is to use `unsigned int` or larger types. – AnT stands with Russia Apr 01 '16 at 00:24
  • `%` is **remainder**, not modulus. The value of the remainder depends on how the result of division involving negative numbers is rounded. A negative remainder occurs when a negative quotient gets rounded toward zero. (In a burst of revisionism, this rule was changed recently, in effect requiring rounding down, which gives non-negative remainders). – Pete Becker Apr 01 '16 at 13:44
2

Arithmetic operations want to operate on an int or larger. So, your byte's are being promoted to integers before they are subtracted -- and, you're likely getting actual int's, which C/C++ is OK with because they can hold the entire range of byte.

If the result of the subtraction is cast back down to byte, it gives you the expected overflow behavior. However, if you omit the cast in the diff2 calculation, you're doing the modulus on a negative int. And, because C/C++ signed division rounds towards zero, the signed modulus has the same sign as the dividend.

The first misstep here is to expect subtraction to act directly on your byte type, or to translate your unsigned byte into an unsigned int. The cascading problem is to overlook the behavior of C++ signed division (which is understandable if you don't know that you should expect signed arithmetic to be an issue in the first place).

Note that, if your RING_BUFFER_SIZE were not a power of two, the division wouldn't work correctly for cases like this anyway. And, since it is a power of two, note that:

(b2 - b1)&(RING_BUFFER_SIZE-1)

should work correctly.

And finally (as suggested in the comment), the right way to do a ring-buffer subtract would be to make sure b1 < RING_BUFFER_SIZE (which makes sense for a ring buffer operation), and use something like:

(b2>b1)? b2 - b1 : RING_BUFFER_SIZE + b2 - b1
comingstorm
  • 25,557
  • 3
  • 43
  • 67