Неявное преобразование в число с плавающей запятой с использованием avr-gcc: uint8_t против uint16_t

У меня есть вопрос относительно неявного преобразования uint8_t и uint16_t с помощью Arduino IDE 1.8.2 (gcc 4.9.2.). Аппаратное обеспечение стандартное Arduino (ATMega328p).

Я написал кусок кода, используя uint8_t, а потом решил переключиться на uint16_t. (Должен был предвидеть это...)

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

Минимальный рабочий пример следующий:

void setup()
{
  uint8_t x = 15;
  uint8_t y = 5;
  float myF = y-x;

  Serial.begin(74880);
  Serial.println(myF);
}

На моей последовательной консоли будет напечатано -10.00.
Это хорошо, чего я и ожидал.

Однако, если я изменю x (или x и y) на uint16_t, результат будет 65526,00! Если я изменю myF с float на int, я снова получу -10. (Я никогда не меняю значения)

Поскольку я сохраняю результат в типе данных со знаком, я предполагаю, что компилятор понимает возможность отрицательных значений и «правильно обрабатывает ситуацию» (сохраняет знак, как в случае с int) или печатает предупреждение, если это не так. не доволен несоответствием типов данных. Однако даже с уровнем предупреждения, установленным на «все», он никогда не показывал предупреждение. Поэтому я предполагаю, что компилятор знает, как справиться с ситуацией, не теряя знак/данные.

Кроме того, поскольку он работает с int в качестве целевого типа данных, меня удивляет, что он не работает с большим числом с плавающей запятой.

Я проверил ситуацию на своей системе x86 - gcc 4.7.3 сохраняет знак. Однако в мире 8-битных микроконтроллеров AVR могут применяться другие правила/условия. (?)

Так что же там происходит? Может быть, кто-то с большим знанием компилятора может помочь здесь..
(Я знаю, что мог бы избежать этой ситуации, явно приведя, но поэтому я должен был знать об этой ловушке.
Итак, я хотел бы знать чем именно это вызвано, так как действительно было неожиданностью при переключении с uint8_t на uint16_t..)

Я прочитал это в соответствии с правила преобразования целых чисел "целочисленные типы, меньшие, чем int, преобразуются в int, когда над ними выполняется операция". Я предполагаю, что avr-gcc следует целочисленным акциям. (?) Итак, я понимаю, что фактическое вычисление обычно выполняется с целым числом, а затем преобразуется в целевой тип данных (в данном случае с плавающей запятой). Проблема в том, что uint16_t имеет такой же размер, но не меньше, чем 16-битный int AVR, и поэтому uint16_t не может быть повышен? Если да, то почему он работает с int в качестве целевого типа?

И почему он работает с целочисленной целевой переменной, но не с 4-байтовым числом с плавающей запятой? А почему не предупреждает?


person random    schedule 13.06.2018    source источник
comment
Если int 16-битное, то uint16_t не меньше. Оба операнда имеют одинаковый тип, поэтому вычитание выполняется с помощью unsigned, а значение переносится четко определенным образом.   -  person Weather Vane    schedule 13.06.2018
comment
Вычисление не имеет ничего общего с float, и компилятор ничего не реализует. Результат присваивается float потом. Вы должны были бы привести один операнд к float перед вычислением, чтобы сделать float соображением.   -  person Weather Vane    schedule 13.06.2018
comment
(да, я знаю, что 1 год и 7 месяцев спустя) Но что забавно, так это то, что «ожидаемый» результат -10 для uint8_t не тот, который я ожидаю. Я бы сказал, что должно быть 246. Ожидается 65526. И «снова -10» — тоже ожидаемый результат. Поскольку вы никогда не используете x и y где-либо еще, возможно, они были оптимизированы.   -  person doom    schedule 30.01.2020


Ответы (2)


Так что же там происходит?

Целочисленные акции

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

В приведенном ниже y-x продвигает каждый x,y до int и вычисляет 5-15, который равен int -10, и присваивает эти значения mF.

  uint8_t x = 15;
  uint8_t y = 5;
  float myF = y-x;

В приведенном ниже примере y-x продвигает каждое x,y до unsigned и вычисляет 5u-15u, которое равно unsigned 65526u, и присваивает эти значения mF.

  uint16_t x = 15;
  uint16_t y = 5;
  float myF = y-x;

Почему unsigned, а не int? uint16_t не соответствует условию, если int может представлять все значения условия исходного типа на 16-битной платформе int при продвижении uint16_t.

Никакой тайны, просто целочисленные рекламные акции на платформе с 16-битным int/unsigned

person chux - Reinstate Monica    schedule 13.06.2018
comment
Ну, это гвозди. (кстати: int -10 присваивается mF.) После пересмотра правил целочисленного преобразования с учетом вашего ответа и комментария @Weather я теперь знаю, что происходит. Я думал, что компилятор считает, что все закончится числом с плавающей запятой, но это было заблуждение. Еще одна вещь, которую следует учитывать при использовании C... Всегда приводить к signed/float вручную, когда подпись может стать необходимой... или очень хорошо протестировать... - person random; 14.06.2018
comment
@random В целом в целом следует избегать приведения типов, поскольку они имеют тенденцию маскировать ошибки или слабый код - часто есть лучшие альтернативы. Но в других случаях это мудро. Это очень зависит от кода. - person chux - Reinstate Monica; 14.06.2018

Вы используете целые числа без знака для перехода в отрицательное значение, приводя эти беззнаковые типы к типу с плавающей запятой, что является неопределенным поведением, определяемым реализацией. Используйте int8_t или int16_t для правильной обработки отрицательных значений. Разница в поведении, когда приведение к float из uint16_t по сравнению с приведением из uint8_t зависит от реализации, поэтому трудно точно сказать, что происходит.

person Thomas Jager    schedule 13.06.2018
comment
Здесь нет неопределенного поведения, но целочисленные преобразования здесь зависят от того, может ли тип int реализации представлять все значения uint16_t или нет. Так что в лучшем случае это поведение, определяемое реализацией. - person Ian Abbott; 13.06.2018
comment
Для простоты я не упомянул, что значения данных, с которыми работает моя программа, всегда являются положительными значениями. Поэтому сохранение их в int16_t вместо uint16_t уменьшит возможное разрешение данных. (и переход к int32_t удвоит размер набора данных). Как уже упоминалось, явное приведение переменных перед вычислением кажется правильным, тем не менее это было для меня неожиданным открытием, поскольку я думал, что целевой тип данных был рассмотрен компилятор и я не ожидал проблем с изменением данных на более широкий тип данных. Но теперь я знаю лучше. - person random; 14.06.2018