Search code examples
c++bitwise-operators

Is bitshifting from an unsigned to a signed smaller type portable?


I have a unsigned short (which is 16 bit on the target platforms)
It contains two 8-bit signed values, one in the lower byte, one in the higher byte.

#include <vector>
#include <iostream>
int main() {
    unsigned short a = 0xE00E;
    signed char b = a & 0xFF;
    signed char c = ((a >> 8) & 0xFF);
    std::cout << (int)b << std::endl;
    std::cout << (int)c << std::endl;

}

Is this portable, or am I relying on platform dependent behaviour here?
On all major compilers (gcc, msvc, clang), the result is 14 and -32, which is the expected output.


Solution

  • Disclaimer: I am no language lawyer

    Is this portable, or am I relying on platform dependent behaviour here?

    Since there is no version specified, I used last draft.

    So what did we need to check:

    1. Can unsigned short hold 0xE00E and signed char can hold 8 bits?
    2. How a & 0xFF and ((a >> 8) & 0xFF) are transformed into signed char?
    3. How signed char is transformed into int?

    1. Can unsigned short hold 0xE00E?

    Type             | Minimum width 
    
    signed char        8    
    short int          16
    int                16
    long int           32
    long long int      64
    

    Source : https://eel.is/c++draft/basic.fundamental

    2. How a & 0xFF and ((a >> 8) & 0xFF) are transformed into signed char?

    In particular, arithmetic operators do not accept types smaller than int as arguments, and integral promotions are automatically applied

    unsigned char or unsigned short can be converted to int if it can hold its entire value range, and unsigned int otherwise;

    Source: https://en.cppreference.com/w/cpp/language/implicit_conversion Integral promotion

    AFAIU, in a & 0xFF, a can be an int or unsigned int.

    If sizeof(unsigned short) == sizeof(int) a will be an unsigned int and an int otherwise.

    If the types are the same, that type is the common type.

    If the unsigned type has conversion rank greater than or equal to the rank of the signed type, then the operand with the signed type is implicitly converted to the unsigned type.

    Source: https://en.cppreference.com/w/c/language/conversion Usual arithmetic conversions

    Now we need to go to signed char from int or unsigned int

    Otherwise, the result is the unique value of the destination type that is congruent to the source integer modulo 2N, where N is the width of the destination type.

    Source: https://eel.is/c++draft/conv.integral#3

    Note: is implementation-defined until C++20 (source https://en.cppreference.com/w/cpp/language/implicit_conversion)

    And the memory representation of an negative signed:

    An unsigned integer type has the same object representation, value representation, and alignment requirements ([basic.align]) as the corresponding signed integer type. For each value x of a signed integer type, the value of the corresponding unsigned integer type congruent to x modulo 2N has the same value of corresponding bits in its value representation. [Example 1: The value −1 of a signed integer type has the same representation as the largest value of the corresponding unsigned type. — end example]

    Source: https://eel.is/c++draft/basic.fundamental#3

    All good.

    signed char b = a & 0xFF;
    signed char c = ((a >> 8) & 0xFF);
    

    Are defined and what you expect.

    3. How signed char is transformed into int?

    It's https://eel.is/c++draft/conv.integral#3 again. And it wont modified the value.

    Conclusion

    Is bitshifting from an unsigned to a signed smaller type portable?

    In C++20

    Yes

    Before C++20

    The conversion unsigned to signed is implementation defined. To prevent this to happen we need to

    static_assert(sizeof(unsigned short) < sizeof(int));
    

    And the code is fully portable.