If one wanted to be really, really explicit, then
uint32_t fred = (uint32_t)((uint8_t)(buf[24]))
| ((uint32_t)((uint8_t)(buf[25])) << 8)
| ((uint32_t)((uint8_t)(buf[26])) << 16)
| ((uint32_t)((uint8_t)(buf[27])) << 24);In this case, each array element is first cast into an 8-bit unsigned integer type. Then, each is cast to a sufficiently large unsigned integer type (best use the same type as the result of the entire expression, although fast unsigned types ––
uint_fast32_t here –– would also work equally well), and then shifted to their final position. The four are then binary-OR'd together to get the final result.
When the cast is to an unsigned integer type, the conversion is done using modulo arithmetic (C11 6.3.1.3p2), regardless of whether the original integer type is signed or unsigned. Thus, a cast to
uint8_t type is effectively equivalent to a binary AND with 255. Similarly, a cast to
uint16_t is effectively equivalent to a binary AND with 65535, and a cast to
uint32_t to a binary AND with 4294967295. (The same applies to all unsigned integer types:
unsigned char and
& UCHAR_MAX,
unsigned short and
& USHRT_MAX,
unsigned int and
& UINT_MAX,
unsigned long and
& ULONG_MAX, and if supported,
unsigned long long and
& ULLONG_MAX.)
The key to remember is that it is modular and not saturating conversion.
Why "effectively equivalent"? The practical result in terms of the C abstract machine are the same, but the cast is often easier for the compiler to optimize than the binary AND.
So I got away with the x[y] << 24 because the 32 bit value (in my application it is stuff like code size; always< 1MB on a 32F417) cannot ever be bigger than 3 bytes. In fact the code would work for any 32 bit value which has bit 31 = 0.
You "got away" with it, because the elements are of unsigned integer type, and your compiler uses modular arithmetic for left shift on the signed
int type.
First, each element gets promoted to
int (but never become negative; they'd be promoted to
unsigned int if the value could not be represented by
int – the C integer promotion rules are quite explicit). Then, each element is shifted left. If the seventh bit in the highest byte is set, we invoke Undefined Behaviour.
To repeat what Newbrain already wrote above, but in a different form just so that readers here understand (because it is kinda important):
Consider the case where
int E1 = 128, and we do
result = E1 << 24. Technically, since
INT_MAX = 2147483647 on your architecture,
E1 << 24 is Undefined Behaviour, because:
The result of E1 << E2 is E1 left-shifted E2 bit positions; vacated bits are filled with zeros. If E1 has an unsigned type, the value of the result is E1 × 2E2, reduced modulo one more than the maximum value representable in the result type. If E1 has a signed type and nonnegative value, and E1 × 2E2 is representable in the result type, then that is the resulting value; otherwise, the behavior is undefined.
We fall in the
otherwise, the behaviour is undefined case, because
E1 is signed (but nonnegative value), but
E1×224 = 231 = INT_MAX+1 > INT_MAX.
However, there is so much code out there relying on the same behaviour that you are, that all current C compilers behave as if
E1 was cast to the corresponding unsigned type first, then the shift applied, and finally the result cast to the original signed type. So, I wouldn't be too worried about a compiler generating wrong machine code (compared to the obvious programmer intent) here, although a strict reading of the C11 standard says it could do anything it wants, including produce nasal daemons. If they generate wrong machine code for this, a lot of other existing code will miscompile too, and not work; compiler users are quite unhappy about such changes, and tend to switch to using a different compiler instead.
Anyway, I don't like the subtext of "getting away with it" here, because really, it is all about what the logic of your expressions is based on.
Put bluntly, I don't think you are "getting away with" something, I just think you are relying on things without knowing you are relying on them, and want to inform you of exactly what you are relying on when using such expressions. (It is not
wrong per se to rely on them, but for long-term maintenance et cetera, you do want to document them at least.)
This is also exactly the reason why I sometimes insist on technically incorrect/incomplete, but intuitively constructive, analogs and explanations.
The worth of intuitively understanding exactly what you are basing your expectations on when writing such seemingly simple expressions in C is, in my opinion, very high. The same applies to pointers and aliasing, and unions and type punning as well. It is what leads to better code, in my opinion.