Left shift and discard bits

Consider a function (one of its possible implementations) that will nullify zero N bits of an unsigned short value (or any other unsigned integral type). A possible implementation might look like this:

template<unsigned int shift> unsigned short zero_right(unsigned short arg) { using type = unsigned short; constexpr type mask = ~(type(0)); constexpr type right_zeros = mask << shift; // <-- error here return arg & right_zeros; } int check() { return zero_right<4>(16); } 

With this code, I have access to all compilers to complain about a possible overflow anyway. CLang is the most explicit, with the following clear message:

error: implicit conversion from 'int' to 'const type' (aka 'const unsigned short') changes the value from 1048560 to 65520 [-Werror, -Wconstant-conversion]

This code looks crisp and clear like a day to me, but when 3 compilers complain, I'm really nervous. Am I missing something? Is there any chance that something suspicious is happening?

PS Although alternative zeriong implementations from the remaining X-bits may be welcome and interesting, the main task of this question is the reliability of the code published.

+6
source share
4 answers

The message seems pretty simple:

error: implicit conversion from 'int' to 'const type' (aka 'const unsigned short') changes the value from 1048560 to 65520 [-Werror, -Wconstant-conversion]

mask << shift has a value of 1048560 (arising from 65535 << 4 ), and you assign it an unsigned short , which is defined to adjust the value of mod 65536 , giving 65520 .

This last transformation is clearly defined. The error message is due to the fact that you passed the compiler flags -Werror,-Wconstant-conversion , requiring you to get an error message in this situation. If you do not want this error, do not skip these flags.


Although this specific use has been clearly defined, there may be undefined behavior for some inputs (namely, shift 16 or more if you are on a 32-bit int system). Therefore, you must fix this feature.

To fix this function, you need to be more careful in the case of unsigned short due to the extremely annoying rule of integers promoting an unsigned short circuit to a signed int.

Here one solution is slightly different from other suggestions .. completely avoid switching problems, works for any shift size:

 template<unsigned int shift, typename T> constexpr T zero_right(T arg) { T mask = -1; for (int s = shift; s--; ) mask *= 2u; return mask & arg; } // Demo auto f() { return zero_right<15>((unsigned short)65535); } // mov eax, 32768 
+2
source

From the C ++ 11 standard:

5.8 Shift operators [expr.shift]

one...

The operands must be integer or non-enumerated types of enumerations, and integral promotions are performed. The result type is the result of the advanced left operand.

Expression

 mask << shift; 

evaluated after applying holistic advertising to mask . Therefore, it evaluates to 1048560 if sizeof(unsigned short) is 2, which explains the message from clang.

One way to avoid the overflow problem is to first shift to the right before performing a left shift and move it to your own function.

 template <typename T, unsigned int shift> constexpr T right_zero_bits() { // ~(T(0)) performs integral promotion, if needed // T(~(T(0))) truncates the number to T, if needed. return (T(~(T(0))) >> shift ) << shift; } template<unsigned int shift> unsigned short zero_right(unsigned short arg) { return arg & right_zero_bits<unsigned short, shift>(); } 
+3
source

Yes, as you suspect, even after suppressing the compiler’s diagnostics, your code, strictly speaking, is not completely ported due to progress from unsigned short to the signed int, the arithmetic bit is executed in the signed int, and then the signed int is converted back unsigned. You managed to avoid undefined behavior (I think after a quick look), but the result is not guaranteed by what you hope for. (type)~(type)0 not required to match "all bits one" in type type ; it already takes place before the shift.

To get something completely portable, just make sure that you do all your arithmetic, at least without the int sign (if necessary, wider types, but not narrower). Then there will be no promotions for signed types to worry about.

 template<unsigned int shift> unsigned short zero_right(unsigned short arg) { using type = unsigned short; constexpr auto mask = ~(type(0) + 0U); constexpr auto right_zeros = mask << shift; return arg & right_zeros; } int check() { return zero_right<4>(16); } 
+3
source

I don't know, this is exactly what you want, but it compiles:

 template<unsigned int shift> unsigned short zero_right(unsigned short arg) { using type = unsigned short; //constexpr type mask = ~(type(0)); type right_zeros = ~(type(0)); right_zeros <<= shift; return arg & right_zeros; } int check() { return zero_right<4>(16); } 

UPDATE:

It looks like you just hushed up the compiler, making sure that it does not know what is happening with the types.

Not

First you get right_zeros with a value of FFFF (from ~0 ). Usually ~0 FFFFFFFFFFFFFF... , but since you use u16 , you get FFFF .

Then a shift of 4 produces FFFF0 [the calculation extends to 32 bits], but when saving back, only the FFF0 16 bits remain, so the value of FFF0

This is completely legal and specific behavior, and you take advantage of truncation. The compiler is not "tricked". In fact, it works great with or without truncation.

You can do right_zeros in u32 or u64 if you want, but then you will need to add right_zeros &= 0xFFFF

If there is undefined behavior (the very essence of my question!), You just made it invisible.

There is no UB based on the totality of your code, no matter what the compiler says.

Actually, Tavyan got it. Use an explicit listing:

 constexpr type right_zeros = (type) (mask << shift); // now clean 

This tells the compiler, among other things, that you want the truncation to be 16 bits.

If there was UB then the compiler should complain.

+2
source

All Articles