#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
непеременных.
Усечение произойдет в любом случае, независимо от того, выдаст компилятор предупреждение или нет.