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;
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.