+= operator for uint16_t promotes the assigned value to int and won't compile

Viewed 4605

This is a real WTF for me, looks like a bug in GCC, but I'd like to have the community have a look and find a solution for me.

Here's the simplest program I could muster:

#include <stdio.h>
#include <stdint.h>

int main(void)
{
 uint16_t i = 1;
 uint16_t j = 2;
 i += j;
 return i;
}

I'm trying to compile this on GCC with -Werror=conversion flag, which I'm using for much of my code.

Here's the result:

.code.tio.c: In function ‘main’:
.code.tio.c:9:7: error: conversion to ‘uint16_t {aka short unsigned int}’ from ‘int’ may alter its value [-Werror=conversion]
  i += j;

Same error would happen for this code:

uint16_t i = 1;
i += ((uint16_t)3);

Error is

.code.tio.c: In function ‘main’:
.code.tio.c:7:7: error: conversion to ‘uint16_t {aka short unsigned int}’ from ‘int’ may alter its value [-Werror=conversion]
  i += ((uint16_t)3);
       ^

Just to be clear, the error here is on the += operator, NOT the cast.

It looks like the operator overloading for the += with uint16_t is messed up. Or am I missing something subtle here?

For your use: MCVE

Edit: Some more of the same:

.code.tio.c:8:6: error: conversion to ‘uint16_t {aka short unsigned int}’ from ‘int’ may alter its value [-Werror=conversion]
  i = i + ((uint16_t)3);

But i = (uint16_t)(i +3); at least works...

5 Answers

The reason for the implicit conversion is due to the equivalency of the += operator with = and +.

From section 6.5.16.2 of the C standard:

3 A compound assignment of the form E1 op= E2 is equivalent to the simple assignment expression E1 = E1 op (E2), except that the lvalue E1 is evaluated only once, and with respect to an indeterminately-sequenced function call, the operation of a compound assignment is a single evaluation

So this:

i += ((uint16_t)3);

Is equivalent to:

i = i + ((uint16_t)3);

In this expression, the operands of the + operator are promoted to int, and that int is assigned back to a uint16_t.

Section 6.3.1.1 details the reason for this:

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

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.

Because a uint16_t (a.k.a. an unsigned short int) has lower rank than int, the values are promoted when used as operands to +.

You can get around this by breaking up the += operator and casting the right hand side. Also, because of the promotion, the cast on the value 3 has no effect so that can be removed:

i =  (uint16_t)(i + 3);

Note however that this operation is subject to overflow, which is one of the reasons a warning is given when there is no cast. For example, if i has value 65535, then i + 3 has type int and value 65538. When the result is cast back to uint16_t, the value 65536 is subtracted from this value yielding the value 2, which then gets assigned back to i.

This behavior is well defined in this case because the destination type is unsigned. If the destination type were signed, the result would be implementation defined.

The argument to any arithmetic operator is subject to the usual arithmetic conversions described in N1570 (latest C11 draft), §6.3.1.8. The passage relevant to this question is the following:

[some rules about floating point types]

Otherwise, the integer promotions are performed on both operands.

So, looking further how the integer promotions are defined, we find the relevant text in §6.3.1.1 p2:

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.

So, even with this code:

i += ((uint16_t)3);

the presence of an arithmetic operator causes the operand to be converted back to int. As the assignment is part of the operation, it assigns an int to i.

This is indeed relevant because i + 3 could actually overflow an uint16_t.

i += ((uint16_t)3);

is equal to(1)

i = i + ((uint16_t)3);

The right-most operand is explicitly converted from int (the type of the integer constant 3) to uint16_t by the cast. After that, the usual arithmetic conversions(2) are applied on both operands of +, after which both operands are implicitly converted to int. The result of the + operation is of type int.

You then try to store an int in a uint16_t which correctly results in a warning from -Wconversion.

A possible work-around if you wish to avoid assigning an int to a uint16_t would be something like this (MISRA-C compliant etc):

i = (uint16_t)(i + 3u);

(1) This is mandated for all compound assignment operators, C11 6.5.16.2:

A compound assignment of the form E1 op= E2 is equivalent to the simple assignment expression E1 = E1 op (E2), except that the lvalue E1 is evaluated only once,

(2) See Implicit type promotion rules for more info about implicit type promotions.

An explanation is found here:

joseph[at]codesourcery.com 2009-07-15 14:15:38 UTC
Subject: Re: -Wconversion: do not warn for operands not larger than target type

On Wed, 15 Jul 2009, ian at airs dot com wrote:

> Sure, it can wrap, but -Wconversion is not for wrapping warnings.

It's for warnings about implicit conversions changing a value; the arithmetic, in a wider type (deliberately or otherwise), does not wrap, but the value gets changed by the implicit conversion back to char. If the user had explicit casts to int in their arithmetic, there could be no doubt that the warning is appropriate.

The warning occurs because the compiler has the computer performs the arithmetic using a larger type than uint16_t (an int, through integer promotion), and placing the value back into a uint16_t could truncate it. For example,

uint16_t i = 0xFFFF;
i += (uint16_t)3;     /* Truncated as per the warning */

The very same applies to separate assignment and addition operators.

uint16_t i = 0xFFFF;
i = i + (uint16_t)3;  /* Truncated as per the warning */

There are many cases where it would be useful to perform integer operations directly on small unsigned integer types. Because the behavior of ushort1 = ushort1+intVal; will in all defined cases be equivalent to coercing intVal to the type of ushort1 and then performing the addition directly on that type, however, the authors of the Standard saw no need to write special rules for that situation. I think they clearly recognized that such behavior was useful, but that they expected that implementations would generally behave in such fashion whether or not the Standard mandated it.

Incidentally, gcc sometimes processes arithmetic on values of type uint16_t in different ways when the result is coerced to uint16_t than in cases where it isn't. For example, given

uint32_t multest1(uint16_t x, uint16_t y)
{
  x*=y;
  return x;
}

uint32_t multest2(uint16_t x, uint16_t y)
{
  return (x*y) & 65535u;
}

The function multest1() appears to consistently perform the multiplication mod 65536 in all cases, but the function multest2 does not. For example, the function:

void tester(uint16_t n, uint16_t *p)
{
  n|=0x8000;
  for (uint16_t i=0x8000; i<n; i++)
    *p++= multest2(65535,i);
  return 0;
}

will be optimized equivalent to:

void tester(uint16_t n, uint16_t *p)
{
  n|=0x8000;
  if (n != 0x8000)
    *p++= 0x8000;
  return 0;
}

but such simplification won't occur when using multest1. I would not consider gcc's mod-65536 behavior as reliable, but the difference in code generation shows that:

  1. Some compilers, including gcc, perform mod 65536 arithmetic directly when a result will be coerced directly to uint16_t, but...

  2. Some compilers process integer overflow in ways that may cause erroneous behavior even if code completely ignores all the upper bits of the result, so a compiler that's trying to warn of all possible UB should flag constructs which compilers would be allowed to process in silly fashion as portability violations.

While there are many statements of the form ushort1+=intval that could not possibly cause overflow, it's easier to squawk at all such statements than identify only those which could actually cause erroneous behavior.

Related