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";
Thanks, Simone
CodePudding user response:
Because of integer promotion.
- 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
CodePudding user response:
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.