5

I have a question about unsigned integer in C/C++. They and the results of operations on them should always be positive or equal to zero but it does not look the case with uint16_t difference. uint are defined in the C++ header cstdint.

The next program takes the "wrong" branch:

uint16_t beg = 7;
uint16_t end = 6;
uint16_t zero = 0;

if (end - beg >= zero) cout << "This branch is always taken.\n";
else cout << "This branch will never be taken.\n";

I tested in my computer (gcc 9.3.0) and on Compiler Explorer with same result.

To fix that, I have to cast the difference to uint16_t:

if (uint16_t(end - beg) >= zero) cout << "This branch is always taken.\n";
else cout << "This branch will never be taken.\n";
ouflak
  • 2,458
  • 10
  • 44
  • 49
  • 2
    Arithmetic operators automatically convert types smaller than `int` to `int` before doing anything with them, see [integral promotion](https://en.cppreference.com/w/cpp/language/implicit_conversion#Integral_promotion). – HolyBlackCat Jan 01 '22 at 08:40
  • 4
    Look up "integer promotions", e.g. https://en.cppreference.com/w/c/language/conversion. Since every `uint16_t` can be represented by `int`, the operands of `-` are promoted to `int`, the subtraction is done as `int` (signed!) and the result has type `int`. – Nate Eldredge Jan 01 '22 at 08:40
  • The result changes with uint32_t – Sebastian Jan 01 '22 at 09:05
  • 1
    It's one of those "Gotcha's" that leads to sad bugs... all works with legitimate inputs until at some inconvenient time when it fails (I see this as a flaw in C++ design) – slashmais Jan 01 '22 at 09:05
  • Thank you very much for explaining the C++ standard requires to promote the operands and the result to signed int. In my opinion, it makes more sense to get uint16_t as result of operations on uint16_t. By the way, I compiled with all the flags on (-Wextra -Wpedantic -Wall -Wconversion) and I did not received any waring. Since my code has to run on Arduino and Raspberry, I need to specify the length of uint. It took one hour to debug that piece of code to me. In my opinion, if C++ committee wants more people to use C++, they should fix those things. Sorry for the rant. – Simone Pernice Jan 03 '22 at 09:49

2 Answers2

9

Because of integer promotion.

In Implicit conversions:

  1. Otherwise, both operands are integers. Both operands undergo integer promotions (see below); then, after integer promotion, one of the following cases applies:

The unsigned type has conversion rank less than the signed type: If the signed type can represent all values of the unsigned type, then the operand with the unsigned type is implicitly converted to the signed type.

Because all uint16_t values can be represented as an int (32 bits), it gets promoted to an int. Because 6 - 7 = -1, the condition is false


Worth noting that if C++ compilers decide to use 16 bits for an int, this doesn't hold anymore. Because not all uint16_t values can be held in a 16 bits int, it doesn't get promoted anymore. 6 - 7 now cause an wrap-around, and become true

  • 3
    Correct for compilers that use 32-bit `int`, i.e., all the ones that you're likely to encounter as a beginner. However, C++ compilers can use just 16 bits for an `int`, and if that's the case, the type of `uint16_t` values will be `unsigned int`, and the result goes the other way. +1. – Pete Becker Jan 01 '22 at 14:29
  • "decide to use 16 bits for an int" sounds a bit strange. Some C++ compilers do for some architectures. – Sebastian Jan 01 '22 at 15:10
  • Thank you very much for explaining the C++ standard requires to promote the operands and the result to signed int. In my opinion, it makes more sense to get uint16_t as result of operations on uint16_t. By the way, I compiled with all the flags on (-Wextra -Wpedantic -Wall -Wconversion) and I did not received any waring. Since my code has to run on Arduino and Raspberry, I need to specify the length of uint. It took one hour to debug that piece of code to me. In my opinion, if C++ committee wants more people to use C++, they should fix those things. Sorry for the rant. – Simone Pernice Jan 03 '22 at 09:52
1

When the bit-width of int (commonly 32) is wider than end, beg (16), end - beg is like (int)end - (int)beg with an int result - potentially negative.

To fix that, I have to cast the difference to uint16_t:

// OP's approach
if (uint16_t(end - beg) >= zero) cout << "This branch is always taken.\n";
else cout << "This branch will never be taken.\n";

That is one way.

Another way to drive code to use unsigned math:

// Suggested alternative
// if (uint16_t(end - beg) >= zero) ...
if (0u + end - beg >= zero) ...

This will cause unsigned math throughout 0u + end - beg >= zero*1.

The trouble with casts is one of code maintenance. Should code later use uint32_t beg, end, consider the now bug uint16_t(end - beg).

True that any local code change warrants a larger review, it is simply that gentle nudges to use unsigned math typically cause less issues and less maintenance than casts. The nudge never narrows. A cast may widened or narrow.


Deeper

The core issues lies with using unsigned types narrower than int/unsigned. When able, avoid this for non-array objects like beg, end, .... When obliged to use small unsigned types that may be narrower than unsigned, take care to insure the desired unsigned/signed math is used on these objects.

From time-to-time, I wanted a uint_N_and_at_least_unsigned_width_t type to avoid this very problem.


*1 Unless beg, end later become a signed integer type wider than unsigned. In which case, usually the wider signed math is preferred.

chux - Reinstate Monica
  • 143,097
  • 13
  • 135
  • 256