c++cstandardsinteger-promotionstdint

Why do implementations of "stdint.h" disagree on the definition of UINT8_C?


The UINT8_C macro is defined in "stdint.h", with the following specification: The macro UINTN_C(value) shall expand to an integer constant expression corresponding to the type uint_leastN_t.

In the wild, however, implementations differ:

#define UINT8_C(value) ((uint8_t) __CONCAT(value, U))  // AVR-libc
#define UINT8_C(x_)    (static_cast<std::uint8_t>(x_)) // QP/C++
#define UINT8_C(c)     c                               // GNU C Library

The first two implementations seem roughly equivalent, but the third one behaves differently: for example, the following program prints 1 with AVR-libc and QP/C++, but -1 with glibc (because right shifts on signed values propagate the sign bit).

std::cout << (UINT8_C(-1) >> 7) << std::endl; // prints -1 in glibc

The implementation of UINT16_C displays the same behavior, but not UINT32_C, because its definition includes the U suffix:

#define UINT32_C(c) c ## U

Interestingly, glibc's definition of UINT8_C changed in 2006, due to a bug report. The previous definition was #define UINT8_C(c) c ## U, but that produced incorrect output (false) on -1 < UINT8_C(0) due to integer promotion rules.

Are all three definitions correct according to the standard? Are there other differences (besides the handling of negative constants) between these three implementations?


Solution

  • If an int can represent all the values of a uint_least8_t then the GNU implementation of the UINT8_C(value) macro as #define UINT8_C(c) c conforms to the C standard.

    As per C11 7.20.4 Macros for integer constants paragraph 2:

    The argument in any instance of these macros shall be an unsuffixed integer constant (as defined in 6.4.4.1) with a value that does not exceed the limits for the corresponding type.

    For example, if UINT_LEAST8_MAX is 255, the following usage examples are legal:

    But the following usage examples result in undefined behavior:

    The signed equivalent INT8_C(-1) is also undefined behavior for the same reasons.

    If UINT_LEAST8_MAX is 255, a legal instance of UINT8_C(value) will expand to an integer constant expression and its type will be int due to integer promotions, as per paragraph 3:

    Each invocation of one of these macros shall expand to an integer constant expression suitable for use in #if preprocessing directives. The type of the expression shall have the same type as would an expression of the corresponding type converted according to the integer promotions. The value of the expression shall be that of the argument.

    Thus for any legal invocation of UINT8_C(value), the expansion of this to value by any implementation where an int can represent all the values of uint_least8_t is perfectly standard conforming. For any illegal invocation of UINT8_C(value) you may not get the result you were expecting due to undefined behavior.

    [EDIT added for completeness] As pointed out in cpplearner's answer, the other implementations of UINT8_C(value) shown in OP's question are invalid because they expand to expressions that are not suitable for use in #if processing directives.