+ = оператор для uint16_t способствует присвоению значения int и не компилируется

Это реальный WTF для меня, похоже на ошибку в GCC, но я хотел бы, чтобы сообщество посмотрело и нашел решение для меня.

Вот простейшая программа, которую я мог бы собрать:

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

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

Я пытаюсь скомпилировать это на GCC с флагом -Werror=conversion, который я использую для большей части моего кода.

Здесь результат:

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

Такая же ошибка для этого кода:

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

Ошибка

.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);
       ^

Чтобы быть ясным, ошибка здесь находится в операторе +=, а не в роли.

Похоже, что перегрузка оператора для += с помощью uint16_t испорчена. Или я пропустил что-то тонкое здесь?

Для использования: MCVE

Изменить: несколько больше:

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

Но i = (uint16_t)(i +3); по крайней мере работает...

Ответ 1

Причина неявного преобразования обусловлена ​​эквивалентностью оператора += с = и +.

Из раздела 6.5.16.2 Стандарт C:

3 Совокупное присвоение формы E1 op = E2 эквивалентно простому присваиванию E1 = E1 op (E2), за исключением того, что значение lvalue E1 оценивается только один раз и по отношению к неопределенно-секвенированный вызов функции, операция составного присвоения представляет собой единую оценку

Итак, это:

i += ((uint16_t)3);

Является эквивалентным:

i = i + ((uint16_t)3);

В этом выражении операнды оператора + продвигаются до int и что int присваивается обратно .

В разделе 6.3.1.1 подробно указывается причина этого:

2 В выражении могут использоваться следующие выражения:26 > или unsigned int:

  • Объект или выражение с целым типом (кроме int или unsigned int), целочисленный ранг преобразования которого меньше или равен ранг int и unsigned int.
  • Битовое поле типа _Bool, int, signed int или unsigned int.

Если int может представлять все значения исходного типа (как ограниченные по ширине, для битового поля), значение преобразуется в значение int; в противном случае он преобразуется в unsigned int. Они называются целые рекламные акции. Все остальные типы не изменяются целым числом акции.

Поскольку a uint16_t (a.k.a. an unsigned short int) имеет более низкий ранг, чем int, значения повышаются при использовании в качестве операндов до +.

Вы можете обойти это, разбив оператор += и выбрав правую сторону. Кроме того, из-за рекламного акта приведение значения 3 не имеет эффекта, поэтому его можно удалить:

i =  (uint16_t)(i + 3);

Обратите внимание, однако, что эта операция подвержена переполнению, что является одной из причин предупреждения, когда нет отливки. Например, если i имеет значение 65535, то i + 3 имеет тип int и значение 65538. Когда результат возвращается к uint16_t, значение 65536 вычитается из этого значения, получая значение 2, которое затем возвращается к i.

Это поведение хорошо определено в этом случае, потому что тип назначения не указан. Если тип назначения был подписан, результат будет определяться реализацией.

Ответ 2

Аргумент любому арифметическому оператору подчиняется обычным арифметическим преобразованиям, описанным в N1570 (последний проект C11), §6.3.1.8. Прохождение, относящееся к этому вопросу, следующее:

[некоторые правила о типах с плавающей запятой]

В противном случае целые рекламные акции выполняются в обоих операндах.

Итак, изучая, как определяются целые рекламные акции, мы находим соответствующий текст в §6.3.1.1 p2:

Если int может представлять все значения исходного типа (ограниченные шириной, для бит-поле), значение преобразуется в int; в противном случае он преобразуется в unsigned int. Они называются целыми рекламными акциями.

Итак, даже с этим кодом:

i += ((uint16_t)3);

наличие арифметического оператора приводит к тому, что операнд будет преобразован обратно в int. Поскольку назначение является частью операции, оно присваивает от int до i.

Это действительно актуально, потому что i + 3 может переполнить uint16_t.

Ответ 3

i += ((uint16_t)3);

равно (1)

i = i + ((uint16_t)3);

Самый правый операнд явно преобразуется из int (тип целочисленной константы 3) в uint16_t приложением. После этого обычные арифметические преобразования (2) применяются к обоим операндам +, после чего оба операнда неявно преобразуются в int. Результат операции + имеет тип int.

Затем вы пытаетесь сохранить int в uint16_t, который корректно выводит предупреждение из -Wconversion.

Возможная обходная ситуация, если вы хотите избежать назначения int в uint16_t, будет примерно такой (MISRA-C совместимый и т.д.):

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

(1) Это задано для всех составных операторов присваивания, C11 6.5.16.2:

Составное присвоение формы E1 op = E2 эквивалентно простому присваиванию E1= E1 > op (E2), за исключением того, что lvalue E1 оценивается только один раз,

(2) См. Правила неявного типа продвижения для получения дополнительной информации о неявных рекламных кампаниях типа.

Ответ 4

Объяснение найдено здесь:

joseph [at] codesourcery.com 2009-07-15 14:15:38 UTC
Тема: Re: -Wconversion: не предупреждать для операндов не более целевого типа

В Ср, 15 июл 2009, ian at airs dot com писал (а):

> Конечно, он может быть обернут, но -Wconversion не для обертывания предупреждений.

Это для предупреждений о неявных преобразованиях, изменяющих значение; арифметика, в более широком типе (преднамеренно или иначе), не обертывается, но значение изменяется с помощью неявного преобразования обратно в char. Если у пользователя были явные приведения к int в их арифметике, не могло быть никакого сомневайтесь, что предупреждение является подходящим.

Предупреждение возникает из-за того, что в компиляторе компьютер выполняет арифметику с использованием более крупного типа, чем uint16_t (a int, путем цельного продвижения), и размещение значения обратно в uint16_t может усечь его. Например,

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

То же самое относится к отдельным операторам присваивания и сложения.

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

Ответ 5

Существует много случаев, когда было бы полезно выполнять целочисленные операции непосредственно с небольшими целыми типами без знака. Поскольку поведение ushort1 = ushort1+intVal; во всех определенных случаях будет равносильно принуждению intVal к типу ushort1, а затем выполнению добавления непосредственно на этом типе, однако, авторы Стандарта не нуждались в написании специальных правил для этой ситуации. Я думаю, они ясно осознали, что такое поведение было полезным, но они ожидали, что реализации, как правило, будут вести себя так, независимо от того, санкционировал ли это стандарт.

Кстати, gcc иногда обрабатывает арифметику по значениям типа uint16_t по-разному, когда результат принуждается к uint16_t, чем в случаях, когда это не так. Например, данный

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;
}

Функция multest1(), по-видимому, во всех случаях последовательно выполняет модификацию мод 65536, но функция multest2 этого не делает. Например, функция:

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

будет оптимизирован эквивалентно:

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

но такое упрощение не произойдет при использовании multest1. Я бы не считал поведение gcc mod-65536 надежным, но разница в генерации кода показывает, что:

  • Некоторые компиляторы, в том числе gcc, выполняют арифметику mod 65536 непосредственно, когда результат будет принудительно применен к uint16_t, но...

  • Некоторые компиляторы обрабатывают переполнение целого числа способами, которые могут вызывать ошибочное поведение, даже если код полностью игнорирует все верхние биты результата, поэтому компилятор, который пытается предупредить обо всех возможных UB, должен указывать конструкции, компиляторы которых разрешены обрабатывать глупо, как нарушения мобильности.

Хотя существует множество операторов вида ushort1 + = intval, которые не могут вызвать переполнение, легче проклинать при всех таких утверждениях, чем идентифицировать только те, которые могут вызвать ошибочное поведение.