In C and C++, the usual arithmetic conversions are a set of implicit conversions performed to match the operand types in built-in binary operations such as addition, subtraction, less-than, etc (note that bit-shifts are not included). These conversions can occasionally result in subtle bugs, such as:
A somewhat contrived example, but the point is that mixing signed and unsigned integers can often result in unexpected conversions. In the expression
n - i of this example, because
n is unsigned and has the same conversion rank as
i is implicitly converted into
unsigned. The result is therefore also
unsigned, causing the loop condition to be trivially satisfied. Therefore, it’s always a good idea to enable compiler warnings for sign-to-unsigned conversions (
-Wsign-conversion in GCC and Clang).
The exact rules are rather long and arcane so I won’t duplicate them here, but the key is recognize that there is actually a rationale behind it, which can be summarized as the following rules:
- Rule A. The final conversion rank is always the larger of the two, no more, no less. The rank is related to the size of integer types, so you’ll never get a type whose size is larger than expected.
- Rule B. The conversion will never cause a signed integer overflow, so undefined behavior won’t occur due to the conversion itself (it can still occur due to the operation, however).
Note: Although Rule A holds for the usual arithmetic conversions, integer promotion does violate Rule A. In addition, the rationale presented here is a “best guess” and doesn’t necessarily correlate with the rationale of the standards committee.
The unfortunate corollary of the two rules is that frequently you will end up with signed-to-unsigned conversions because the compiler is actively avoiding a potential signed overflow while preserving Rule A. When can this happen? When both of the following hold:
- Naturally, one operand needs to be unsigned and the other operand needs to be signed.
- The type of the signed operand can’t represent every possible value in the type of the unsigned operand. That is, when an overflow of the signed operand would otherwise occur.
In fact, this is really the only scenario that you have to worry about as far as the usual arithmetic conversions are concerned (for integers). In all other cases, the value is preserved by this conversion and hence you won’t see any surprises.
Note: I haven’t actually said anything about floating-point numbers. They are like a trump card: any time a floating-point number is present, the conversion will favor the most precise floating-point number. This is not quite as bad as the signed-to-unsigned conversions, but it can still be lossy if you have very large integers.
Official reference: N1570 Section 188.8.131.52.