2950. std::byte operations are misspecified

Section: 17.2.5 [support.types.byteops] Status: C++20 Submitter: Thomas Köppe Opened: 2017-03-24 Last modified: 2021-02-25

Priority: 1

View all issues with C++20 status.

Discussion:

The operations for std::byte (17.2.5 [support.types.byteops]) are currently specified to have undefined behaviour in general cases, since the type of the expression expr that appears in return byte(expr) is obtained by the arithmetic conversion rules and has higher conversion rank than unsigned char. Therefore, the value of the expression may be outside the range of the enum (for example, consider ~0), and by 7.6.1.9 [expr.static.cast] p10 the conversion results in undefined behaviour.

I believe the original intent of the specification could be expressed correctly with the following, more verbose sequence of casts. I will only give one representative example:

byte operator<<(byte b, IntType shift)

Equivalent to: return byte(static_cast<unsigned char>(static_cast<unsigned char>(b) << shift));

[ 2017-06-27 P1 after 5 positive votes on c++std-lib. ]

[2017-06-28, STL comments and provides wording]

This proposed resolution performs its work in unsigned int, which is immune to promotion. For op=, I'm avoiding unnecessary verbosity.

It stylistically uses static_casts instead of functional-style casts. All of the static_casts are intentional, although not all of them are strictly necessary. I felt that it was simpler to always follow the same pattern for type conversions, instead of skipping static_casts by taking advantage of the possible ranges of values. (I could prepare an alternative PR to avoid unnecessary casts.) I'm not static_casting the shift arguments, because of how 7.6.7 [expr.shift] works.

For to_integer(), there's a tiny question. According to 7.6.1.9 [expr.static.cast]/9, static_casting from [128, 255] bytes to signed (behavior) chars triggers unspecified behavior, whereas using unsigned char as an intermediate type would produce implementation-defined behavior, 7.3.9 [conv.integral]/3. This question is basically theoretical, and it's unaffected by this proposed resolution (which is just changing a functional-style cast to a static_cast).

[2016-07, Toronto Thursday night issues processing]

Status to Ready

Proposed resolution:

This wording is relative to N4659.

  1. Edit 17.2.5 [support.types.byteops] as indicated:

    template <class IntType>
      constexpr byte& operator<<=(byte& b, IntType shift) noexcept;
    

    -1- Remarks: This function shall not participate in overload resolution unless is_integral_v<IntType> is true.

    -2- Effects: Equivalent to: return b = b << shiftbyte(static_cast<unsigned char>(b) << shift);

    template <class IntType>
      constexpr byte operator<<(byte b, IntType shift) noexcept;
    

    -3- Remarks: This function shall not participate in overload resolution unless is_integral_v<IntType> is true.

    -4- Effects: Equivalent to: return static_cast<byte>(static_cast<unsigned char>(static_cast<unsigned int>(b) << shift))byte(static_cast<unsigned char>(b) << shift);

    template <class IntType>
      constexpr byte& operator>>=(byte& b, IntType shift) noexcept;
    

    -5- Remarks: This function shall not participate in overload resolution unless is_integral_v<IntType> is true.

    -6- Effects: Equivalent to: return b = b >> shiftbyte(static_cast<unsigned char>(b) >> shift);

    template <class IntType>
      constexpr byte operator>>(byte b, IntType shift) noexcept;
    

    -7- Remarks: This function shall not participate in overload resolution unless is_integral_v<IntType> is true.

    -8- Effects: Equivalent to: return static_cast<byte>(static_cast<unsigned char>(static_cast<unsigned int>(b) >> shift))byte(static_cast<unsigned char>(b) >> shift);

    constexpr byte& operator|=(byte& l, byte r) noexcept;
    

    -9- Effects: Equivalent to:

    return l = l | rbyte(static_cast<unsigned char>(l) | static_cast<unsigned char>(r));
    
    constexpr byte operator|(byte l, byte r) noexcept;
    

    -10- Effects: Equivalent to:

    return static_cast<byte>(static_cast<unsigned char>(static_cast<unsigned int>(l) | 
    static_cast<unsigned int>(r)))byte(static_cast<unsigned char>(l) | 
    static_cast<unsigned char>(r));
    
    constexpr byte& operator&=(byte& l, byte r) noexcept;
    

    -11- Effects: Equivalent to:

    return l = l & rbyte(static_cast<unsigned char>(l) & static_cast<unsigned char>(r));
    
    constexpr byte operator&(byte l, byte r) noexcept;
    

    -12- Effects: Equivalent to:

    return static_cast<byte>(static_cast<unsigned char>(static_cast<unsigned int>(l) & 
    static_cast<unsigned int>(r)))byte(static_cast<unsigned char>(l) & 
    static_cast<unsigned char>(r));
    
    constexpr byte& operator^=(byte& l, byte r) noexcept;
    

    -13- Effects: Equivalent to:

    return l = l ^ rbyte(static_cast<unsigned char>(l) ^ static_cast<unsigned char>(r));
    
    constexpr byte operator^(byte l, byte r) noexcept;
    

    -14- Effects: Equivalent to:

    return static_cast<byte>(static_cast<unsigned char>(static_cast<unsigned int>(l) ^ 
    static_cast<unsigned int>(r)))byte(static_cast<unsigned char>(l) ^ 
    static_cast<unsigned char>(r));
    
    constexpr byte operator~(byte b) noexcept;
    

    -15- Effects: Equivalent to: return static_cast<byte>(static_cast<unsigned char>(~static_cast<unsigned int>(b)))byte(~static_cast<unsigned char>(b));

    template <class IntType>
      constexpr IntType to_integer(byte b) noexcept;
    

    -16- Remarks: This function shall not participate in overload resolution unless is_integral_v<IntType> is true.

    -17- Effects: Equivalent to: return static_cast<IntType>IntType(b);