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

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


Solution 1:

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.

Solution 2:

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.

Solution 3:

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.

Solution 4:

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 */