Компилятор изменяет тип переменной типа с uin16_t на int, когда она помечена как constexpr

#c #constexpr

Вопрос:

Я столкнулся со странной проблемой, пытаясь перевернуть все биты моего номера.

 #include <cstdint>

constexpr uint16_t DefaultValueForPortStatus { 0xFFFF };

void f(uint16_t x)
{

}

int main()
{
  f(~(DefaultValueForPortStatus));
}
 

Когда я компилирую эту программу (магистраль GCC) Я получаю сообщение об ошибке:

предупреждение: преобразование без знака из «int» в «uint16_t» {он же «короткий без знака int»} изменяет значение с «-65536″ на » 0 » [- Woverflow]

Когда я удаляю constexpr из спецификатора типа, предупреждение не появляется. Это почему? Почему переменная uint16_t constexpr изменяется компилятором на int, тогда как в случае не-constexpr все в порядке?

Комментарии:

1. Разве bit не отменяет ~ продвижение int для небольших типов?

2. «Компилятор изменяет тип переменной типа с uin16_t на int, когда она помечена как constexpr» —> Изменение в > ~(DefaultValueForPortStatus) от типа uint16_t на int не зависит constexpr .

3. Я бы сказал, что в целом это причина для использования оператора XOR, а не оператора not, поэтому вы контролируете, сколько битов инвертируется, а не оставляете это на усмотрение нелогичных правил продвижения C,

Ответ №1:

Это вызвано ИМХО довольно неудачным правилом C о продвижении целых чисел. В нем в основном говорится, что все типы, меньшие, чем int всегда повышаются до int if int , могут представлять все значения исходного типа. Только если нет, unsigned int выбирается. std::uint16_t на стандартных 32/64-разрядных архитектурах относится к первой категории.

int гарантированно будет иметь ширину не менее 16 бит, если бы это было так, unsigned int было бы выбрано, поэтому поведение кода определяется реализацией.

Я точно не знаю, почему компилятор выдает предупреждение только для constexpr значений, скорее всего, потому, что он может легко распространить эту константу через ~ . В других случаях кто-то может изменить DefaultValueForPortStatus какое-то «безопасное» значение, которое не будет переполняться при отрицании и преобразовании int обратно в std::uint16_t . Но проблема существует независимо от постоянства, вы можете проверить это с помощью этого кода:

 #include <type_traits>
#include <cstdint>

constexpr uint16_t DefaultValueForPortStatus { 0xFFFF };

int main()
{
  auto x = ~(DefaultValueForPortStatus);
  static_assert(std::is_same_v<decltype(x), int>);
}
 

Соответствующие стандартные разделы:

  • expr.conv.prom-7.3.7 — Первые несколько абзацев.
  • expr.unary.op-7.6.2.2.10 — В последнем абзаце говорится, что применяется «Целочисленное продвижение».
  • expr.arith.conv-7.4 — применяется только для двоичных операторов, по-прежнему содержит аналогичные правила.

Комментарии:

1. Да, почти наверняка из-за постоянного распространения или его отсутствия, что означает, что GCC не увидит, что конкретное значение усекается во время вычисления во время компиляции. Вы увидите то же самое с static вместо constexpr , потому что это позволит ему увидеть, что никакой другой код в этом блоке перевода не изменяет var и, следовательно, это константа времени компиляции. Без того или другого нет оснований предполагать, что глобальный var все еще имеет значение, которым он был статически инициализирован при main выполнении этой части. (Статический конструктор в другом TU мог бы изменить его.)

2. @PeterCordes На самом деле, static по- видимому , недостаточно, хотя у компиляторов нет проблем с оптимизацией с его помощью (и усечением возвращаемого значения до 0). const работает. Может быть, предупреждения запускаются до const неоптимизации? Не уверен. Но хорошая заметка о том, что другие ТУ меняют значение, я об этом вообще не думал.

3. Ух ты, я удивлен. Может быть, это как-то связано с тем, что я не предупреждал return (uint16_t)~((uint16_t)0xFFBB) ни return (uint16_t)~DefaultValueForPortStatus о том, ни о другом. Например, только тогда, когда он думает, что вы действительно хотели, чтобы он был постоянным, а не во всех случаях, когда он может выполнять постоянное распространение. А также не случаи, когда константа находится прямо в выражении, чтобы вы могли ее видеть? gcc и clang, похоже, договорились о том, предупреждать или нет ( godbolt.org/z/f7x8onP17 ) даже с -Wall -pedantic -Wextra -fsanitize=undefined (это не УБ, так что неудивительно, что УБсан ничего не сделал).

4. @PeterCordes Я вроде как понимаю return (uint16_t)~((uint16_t)0xFFFF); , что вы не генерируете никаких предупреждений, вы явно их разыгрываете. uint16_t foo(){return ~((uint16_t)0xFFFF); } генерирует предупреждение. Я только что заметил, что предупреждение вызывается -Wconstant-conversion в лязге, что также может объяснить это 😀

Ответ №2:

Из-за продвижения целых чисел, на платформах, на которых int 32 бита, выражение

~(DefaultValueForPortStatus)

соответствует значению an int со значением -65536 .

Что именно происходит, так это следующее:

Перед выполнением побитовой операции NOT ( ~ ) операнд DefaultValueForPortStatus получает значение an int , так что его представление в памяти будет эквивалентно представлению an unsigned int со следующим значением:

0x0000FFFF

После применения к нему побитового оператора-НЕ ( ~ ) его представление в памяти будет эквивалентно представлению an unsigned int со следующим значением:

0xFFFF0000

Однако результат имеет тип данных int , а не unsigned int . (Я использую только эквивалентные значения unsigned int для иллюстрации представления памяти.) Следовательно, фактическое значение результата равно -65536 (поскольку C требует, чтобы для целых чисел со знаком использовалось представление памяти с дополнением два).

При преобразовании этого int uint16_t параметра в соответствие с типом параметра функции 16 наиболее значимых битов отбрасываются, а 16 наименее значимых битов сохраняются. Следовательно, аргумент функции будет иметь значение 0 .

При компиляции с использованием настроек по умолчанию gcc выдает предупреждение о таких усечениях, если они встречаются в постоянном выражении. Это предупреждение можно отключить с -Wno-overflow помощью опции командной строки.

Причина, по которой компилятор предупреждает по умолчанию, вероятно, заключается в том, что он предполагает, что такие усечения не предназначены для использования в постоянных выражениях. Он не делает этого предположения с выражениями, основанными на const непеременных.

Усечение произойдет в любом случае, независимо от того, выдаст компилятор предупреждение или нет.