4

The follow derives from the devkitpro 3ds filter audio example though it's not relevant to know exactly how the example works.

In short, consider this code:

size_t data_size = ...;
uint32_t data* = (uint32_t*) malloc(data_size);

for (int i = 0; i < data_size; i++){
  int16_t value = 0xBEEF;
  // IMPORTANT:
  data[i] = (value << 16) | (value & 0xFFFF)
  // This results in
  // data[i] & 0x0000FFFF == value AND
  // data[i] & 0xFFFF0000 == value
}

In this sample, value is shifted left 16 bits despite being a 16 bit type, this should result in zero but it doesn't, presumably because value is cast to uint32 BEFORE the bitwise operation.

What is causing value to be cast to 32 bit BEFORE the bitwise operation value << 16?
Is it data[i] being 32 bit or the 16 literal being 32 bit? Is that even the cause here?

5
  • 3
    For all arithmetic expressions, and shifting is one of them, integer types smaller than int will be promoted to int. So value << 16 is essentially the same as ((int) value) << 16. The value 16 is already an int. Commented Sep 28 at 22:05
  • Also, with value == 0x0000BEEF, then value << 16 will be 0xBEEF0000. and value & 0xFFFF will be 0x0000BEEF. That leads to 0xBEEF0000 | 0x0000BEEF which is is equal to 0xBEEFBEEF. Commented Sep 28 at 22:07
  • 5
    On a different topic, in C you should not cast the result of malloc (or any function returning the type void *). Commented Sep 28 at 22:10
  • 1
    IF C did shift in the source type int16_t, the result would NOT be guaranteed zero; shifting with a count greater than or equal to the width (number of nonpadding bits) is Undefined Behavior. Also, treating a malloc of data_size bytes as large enough to contain data_size uint32_t elements is seriously wrong on any implementation that has int16_t (and nearly all others as well). Commented Sep 29 at 5:45
  • Terminology note: at the risk of pedantry, casting is an operation involving typecast operator. It follows that there cannot be any implicit casts. The generic term for the effect is a type conversion, of which type promotions are a special case. There can be and are automatic / implicit type conversions and promotions. Commented Sep 29 at 13:49

1 Answer 1

7

This happened as the result of integer promotions. In most cases where an integer type smaller than int is used in an expression, it is first promoted to type int. On most platforms, this is a 32-bit type.

This is spelled out in section 6.3.1.1p2 of the C24 (draft) standard:

The following may be used in an expression wherever an int or unsigned int may be used:

  • An object or expression with an integer type (other than int or unsigned int) whose integer conversion rank is less than or equal to the rank of int and unsigned int.
  • A bit-field of type bool, int, signed int, or unsigned int.

The value from a bit-field of a bit-precise integer type is converted to the corresponding bit-precise integer type. If the original type is not a bit-precise integer type (6.2.5): if an int can represent all values of the original type (as restricted by the width, for a bit-field), the value is converted to an int otherwise, it is converted to an unsigned int. These are called the integer promotions. All other types are unchanged by the integer promotions

So assuming an int is at least 32 bits on your platform, a uint16_t has a smaller rank and therefore the value is promoted to type int.


There is still a problem however. In the initialization int16_t value = 0xBEEF, the value to set doesn't fit in a int16_t and so gets converted to a negative value. This negative value is promoted to type int in the expression value << 16 and that negative value is left shifted. Left-shifting a negative value triggers undefined behavior.

Changing the type of value to uint16_t isn't enough. If that's all that is done, the positive value 0xBEEF left-shifted by 16 won't fit into a signed int and again triggers undefined behavior.

An explicit cast to an unsigned type is required here to have well defined behavior. Also note that the bitwise AND is unnecessary since no bits will be stripped off.

uint16_t value = 0xBEEF;
data[i] = ((uint32_t)value << 16) | value;
Sign up to request clarification or add additional context in comments.

9 Comments

Leftshift of a negative value is UB only if it overflows, and 0x[F...]BEEF0000-as-negative in 32-or-more-bit int does not. Shifting the unsigned version promoted to positive 32-bit does overflow, although I doubt you could find a machine operating this decade that actually fails.
Left shifting a negative value is UB as per C24 6.5.8p4 (C11 6.5.7p4).
Left-shifting a non-negative value of a signed integer type has UB if and only if the mathematical result is not representable in the appropriate (signed) type, which could be described as "if it overflows". Left-shifting negative values always has UB. And if you want to think of it in terms of bit strings, left-shifting a negative integer always does overflow. In contrast, left-shifting a value of unsigned (promoted) integer type by a non-negative number of bits less than the width of the type always has well defined behavior.
Specifically, the C standard speaks of width, which is a formal term that in case of signed numbers means value and sign bits. We cannot left shift anything beyond the specified width. And in case of negative numbers, the MSB sign bit is always set, meaning a left shift of that will always go beyond the width of the object.
@ikegami Hello, comments about value & 0xFFFF are incorrect! When this is not done, there is a change in the programs output
Does it still give the wrong value after making all of the above fixes?
It's incorrect to leave out & 0xFFFF in your program, but my comment was about dbush's (since I specifically said it was because value was unsigned). You can see there's no change in the output over here. But since dbush removed the & 0xFFFF from their answer as I suggested, I'm deleting my earlier comment about it.
"... the value to set doesn't fit in a int16_t and so gets converted to a negative value." --> Detail: This is the common case. The conversion is implementation defined: "'the new type is signed and the value cannot be represented in it; either the result is implementation-defined or an implementation-defined signal is raised."
With C23, we could do something like data[i] = ((typeof(data[i]) value << 16) | value;.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.