Является ли (int32_t) 255 << 24 неопределенным поведением в gcc (с ++ 11)?

В C++ 11, согласно en.cppreference.com,

For signed and non-negative a, the value of a << b is a * 2b if it is representable in the return type, otherwise the behavior is undefined.

Насколько я понимаю, поскольку 255 * 224 не представлен как int32_t, оценка (int32_t) 255 << 24 дает неопределенное поведение. Это правильно? Это может быть зависит от компилятора? Это среда IP16, если это важно.

Предыстория: это исходит от аргумент, который у меня есть с пользователь arduino.stackexchange.com. По его словам, «нет ничего undefined об этом »:

you notice that much of the bit shifting is "implementation defined". So you cannot quote chapter-and-verse from the specs. You have to go to the GCC documentation since that is the only place that can tell you what actually happens. gnu.org/software/gnu-c-manual/gnu-c-manual.html#Bit-Shifting - it's only "undefined" for a negative shift value.


Редактировать: Судя по полученным ответам, я правильно понимаю в Стандарт C++ 11. Тогда ключевая часть моего вопроса: это выражение вызывает неопределенное поведение в gcc. Как выразился Давмак в его комментарии я спрашиваю, «определяет ли реализация GCC поведение, даже если оно не определено языковым стандартом ».

Из руководства gcc, на которое я ссылался, кажется, что это действительно определено, хотя я считаю, что формулировка этого руководства больше похожа на учебное пособие чем «языковой закон». Из ответа PSkocik (и комментария Кейна к этому ответ), вместо этого может показаться, что он не определен. Так что я все еще сомневаюсь.

Думаю, моей мечтой было бы иметь четкое заявление в каком-нибудь gcc документация, в которой говорится, что 1) gcc не определяет никакого поведения который явно не определен в стандарте или 2) gcc действительно определяет это поведение из версии XX.XX и обязуется сохранять его определенным во всех последующие версии.

Редактировать 2: PSkocik удалил свой ответ, что я считаю неудачным, потому что он предоставил интересную информацию. Из его ответа, комментарий Кейна к ответ и мои собственные эксперименты:

  1. (int32_t)255<<24 выдает ошибку времени выполнения при компиляции с clang и -fsanitize=undefined
  2. тот же код не вызывает ошибок с g ++ даже с -fsanitize=undefined
  3. (int32_t)256<<24 выдает ошибку времени выполнения при компиляции с g++ -std=c++11 -fsanitize=undefined

Пункт 2 согласуется с интерпретацией, согласно которой gcc в режиме C++ 11 определяет левый сдвиг шире, чем стандарт. Согласно пункту 3, это определение могло быть просто определением C++ 14. Однако пункт 3 непоследовательный с идеей, что указанное руководство является полное определение << в gcc (режим C++ 11), как это указано в руководстве нет намека на то, что (int32_t)256<<24 может быть неопределенным.

«неопределенное поведение - это то, к чему стандарт не предъявляет никаких требований». что означает, что это может быть даже ожидаемое / правильное поведение. И компилятор может делать это с некоторыми ограничениями. Согласно вашей ссылке, GCC хотел сделать ее неопределенной только для отрицательных значений.

P.W 17.12.2018 12:15

В справочнике GNU C говорится: «Для обоих << и >>, если второй операнд больше, чем разрядность первого операнда, или второй операнд отрицательный, поведение не определено». В какой-то степени это означает, что тогда ответ на ваш вопрос - «да». Но это могло не быть исчерпывающим; об этом прямо не говорится, и я думаю, что было бы опасно предполагать, что «да» - это ответ.

davmac 17.12.2018 13:54

Также обратите внимание, что справочные документы GNU C C, а не C++.

davmac 17.12.2018 13:55

В стандартной цитате C (++), которую вы опубликовали, довольно четко указано, что это UB, так почему вы ставите ее под сомнение по поводу того, что кто-то сказал в Интернете?

PSkocik 17.12.2018 13:57

@PSkocik мне ясно, что OP спрашивает, определяет ли GCC, реализация, поведение, даже если оно не определено стандартом языка.

davmac 17.12.2018 13:58

Вы оба правы. На Arduino он будет иметь предсказуемое и повторяемое значение. Но стандарт C++ не скажет вам, каким будет это значение. Еще одно обычное место, где вы можете найти код, намеренно использующий UB, - это реализация CRT.

Hans Passant 17.12.2018 16:10

@HansPassant: По поводу «у него будет предсказуемое и повторяемое значение»: я все еще ищу убедительный аргумент в пользу того, что это правда, в текущих средах и все будущее Arduino. Я готов принять как должное, что Arduino будет придерживаться gcc, но не конкретной версии gcc.

Edgar Bonet 17.12.2018 16:27
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
10
7
496
4

Ответы 4

«Неопределенное поведение - это то, к чему стандарт не предъявляет никаких требований». что означает, что это может быть даже ожидаемое / правильное поведение. В этом стандарте C++ говорится об операторах сдвига.

8.5.7 Shift operators [expr.shift] (C++ Standard draft N4713)

  1. The operands shall be of integral or unscoped enumeration type and integral promotions are performed. The type of the result is that of the promoted left operand. The behavior is undefined if the right operand is negative, or greater than or equal to the length in bits of the promoted left operand. Otherwise, if E1 has a signed type and non-negative value, and E1×2E2 is representable in the corresponding unsigned type of the result type, then that value, converted to the result type, is the resulting value; otherwise, the behavior is undefined.

Как отмечает @rustyx ниже, «Формулировка« E1×2E2 может быть представлена ​​в соответствующем беззнаковом типе типа результата »- это C++ 14. К сожалению, все еще UB в C++ 11».

Однако это не отвечает на вопрос. Если бы это были единственные способы, которыми сдвиг может иметь неопределенное поведение, то ответ на вопрос OP был бы «нет»; но из этой цитаты не ясно, так ли это.

davmac 17.12.2018 13:59

(Примечательно, что вы пропустили: В противном случае, если E1 имеет тип со знаком и неотрицательное значение, а E1 × 2 E2 может быть представлен в соответствующем беззнаковом типе типа результата, то это значение, преобразованное в тип результата, является результирующим значением; в противном случае поведение не определено).

davmac 17.12.2018 14:01

@davmac: Это добавлено сейчас. OP, похоже, уже знал эту часть, поэтому не добавил ее изначально.

P.W 17.12.2018 14:02

Формулировка «E1 × 2E2 может быть представлена ​​в соответствующем беззнаковом типе типа результата» - это C++ 14. Еще UB в C++ 11 к сожалению.

rustyx 17.12.2018 14:09

Конечно. Пожалуйста, сделай. BTW целочисленное переполнение со знаком будет полностью определено в C++ 20 (наконец, комитет убежден, что существует только арифметика дополнения до двух).

rustyx 17.12.2018 14:12

Извините, я взял правильный абзац из неправильной версии документа. Да, это не определено - вот чего критически не хватало. (Но также я думаю, что просто цитирование стандарта все еще не учитывает суть вопроса).

davmac 17.12.2018 14:16

@rustyx, если вы не знаете чего-то, чего я не знаю (что возможно!), подписанное переполнение по-прежнему будет неопределенным поведением в C++ 20, несмотря на то, что для целых чисел требуется представление дополнения до 2. См. Примечания к редакции предложения на open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0907r4.html

davmac 17.12.2018 15:08

@davmac спасибо, что указали на предложение. Я исправился. Я просмотрел только последнюю формулировку expr.shift / 2, которая показалась многообещающей.

rustyx 17.12.2018 16:36

@davmac: К сожалению, Стандарт не предоставляет никаких средств, с помощью которых реализации могут обещать поддерживать некоторые из наиболее полезных гарантий, вытекающих из поведения молчаливого переноса, например гарантия того, что целочисленные операторы (кроме операторов составного присваивания) не будут иметь побочных эффектов, за исключением возможного запуска ловушки, определяемой реализацией. Во многих случаях гарантия того, что операция даст определенный конкретный результат, может быть дорогостоящим, но гарантия того, что она будет продолжаться без побочных эффектов, практически ничего не будет стоить.

supercat 17.12.2018 20:28

Для компиляторов C++ до C++ 14, если у вас была такая функция:

// check if the input is still positive,
// after a suspicious shift

bool test(int input)
{
    if (input > 0) 
    {
        input = input << 24;

        // how can this become negative in C++11,
        // unless you are relying on UB?

        return (input > 0); 
    }
    else 
    {
        return false;
    }
}

тогда оптимизирующий компилятор мог бы изменить его на это:

bool test(int input)
{
    // unless you are relying on UB, 
    // input << 24 must fit into an int,
    // and input is positive in that branch

    return (input > 0);
}

И все счастливы, потому что вы получаете хорошее ускорение в ваших релизных сборках.

Я, однако, не знаю компиляторов, которые на самом деле делают такую ​​оптимизацию для сдвига влево, хотя довольно часто дополнения оптимизируются, например видно в этом примере.

Спасибо за ответ, но я уже в курсе. Я уже безуспешно пытался заставить gcc оптимизировать if (x>=0 && (x<<24)<0) ....

Edgar Bonet 17.12.2018 15:02

О, ладно, тогда у меня создалось впечатление, что вы можете скомпилировать для Arduino, используя разные компиляторы (в конце концов, есть люди, использующие ржавчина), но если это специфично для g ++, тогда стандарты в основном не имеют значения.

Groo 17.12.2018 15:18

Я предполагаю, что вы можете использовать разные компиляторы, но поскольку среда Arduino использует gcc, это своего рода «стандартный компилятор Arduino». И стандарт C++ 11 по-прежнему актуален, поскольку предполагается, что gcc является «совместимой реализацией».

Edgar Bonet 17.12.2018 15:24

Что ж, FWIW, ваш аргумент в связанном потоке действителен, этот код - это UB в C++ 11. Он не переносится, и будущая версия gcc может (теоретически) сломать его с помощью -std=c++11. Я говорю теоретически, потому что программисты настолько привыкли пренебрегать стандартами, что такое изменение, вероятно, сломало бы огромное количество существующего кода. И решение - это просто преобразование uint32_t и обратно, поэтому я не понимаю, почему этот парень против того, чтобы сделать его портативным. Я бы сравнил это с принуждением к использованию встроенных функций gcc вместо стандартных переносимых конструкций.

Groo 17.12.2018 15:41

Большинство оптимизаций, связанных с переполнением, можно включить, заявив, что компиляторам разрешено обрабатывать арифметические операции со знаком с использованием произвольных расширенных целочисленных типов по своему выбору при отсутствии преобразований или приведений типов. Многие из остальных можно включить, разрешив компиляторам также увеличивать длину хранилища, используемого для объектов целочисленного типа со знаком, адрес которых не используется. Даже если код предназначен для использования только с -fwrapv или его эквивалентом, добавление приведения типа if ((int)(x << y) < 0) сделало бы смысл кода более понятным для человека, поэтому ...

supercat 17.12.2018 18:05

... наличие компилятора, оптимизирующего (x<<y)<0 до x<0, когда нет приведения, не сломало бы огромное количество кода, даже если бы этот код был нацелен на простые компиляторы, которые могли бы генерировать оптимальный код только в том случае, если бы задано выражение, которое сдвигается влево и проверяет знак bit (поскольку такой код в любом случае должен включать приведение). Более серьезная проблема связана с компиляторами, где целочисленная арифметика может не только вести себя так, как если бы она давала результаты больше, чем int, но и могла иметь произвольные побочные эффекты для окружающего кода.

supercat 17.12.2018 18:10

Это изменилось со временем, и не без оснований, так что давайте пройдемся по истории. Обратите внимание, что во всех случаях простое выполнение static_cast<int>(255u << 24) всегда было определенным поведением. Может быть, просто сделай это и уведи все проблемы в сторону.


Исходная формулировка C++ 11 была:

The value of E1 << E2 is E1 left-shifted E2 bit positions; vacated bits are zero-filled. If E1 has an unsigned type, the value of the result is E1×2E2, reduced modulo one more than the maximum value representable in the result type. Otherwise, if E1 has a signed type and non-negative value, and E1×2E2 is representable in the result type, then that is the resulting value; otherwise, the behavior is undefined.

255 << 24 - это неопределенное поведение в C++ 11, потому что результирующее значение невозможно представить как 32-разрядное целое число со знаком, оно слишком велико.

Это неопределенное поведение вызывает некоторые проблемы, потому что constexprдолжен диагностирует неопределенное поведение - и поэтому некоторые общие подходы к установке значений приводят к серьезным ошибкам. Следовательно, CWG 1457:

The current wording of 8.8 [expr.shift] paragraph 2 makes it undefined behavior to create the most-negative integer of a given type by left-shifting a (signed) 1 into the sign bit, even though this is not uncommonly done and works correctly on the majority of (twos-complement) architectures [...] As a result, this technique cannot be used in a constant expression, which will break a significant amount of code.

Это был дефект, примененный к C++ 11. Технически соответствующий компилятор C++ 11 будет реализовывать все отчеты о дефектах, поэтому было бы правильно сказать, что в C++ 11 это неопределенное поведение нет; поведение 255 << 24 в C++ 11 определено как -16777216.

Формулировку после дефекта можно увидеть в C++ 14:

The value of E1 << E2 is E1 left-shifted E2 bit positions; vacated bits are zero-filled. If E1 has an unsigned type, the value of the result is E1×2E2, reduced modulo one more than the maximum value representable in the result type. Otherwise, if E1 has a signed type and non-negative value, and E1×2E2 is representable in the corresponding unsigned type of the result type, then that value, converted to the result type, is the resulting value; otherwise, the behavior is undefined.

Не было изменений в формулировках / поведении в C++ 17.

Но для C++ 20 в результате Знаковые целые числа - это дополнение до двух (и его формулировка) формулировка значительно упрощен:

The value of E1 << E2 is the unique value congruent to E1×2E2 modulo 2N, where N is the range exponent of the type of the result.

255 << 24 по-прежнему имеет определенное поведение в C++ 20 (с тем же результирующим значением), просто спецификация того, как мы туда добираемся, становится намного проще, потому что язык не должен обходить тот факт, что представление для целых чисел со знаком был определен реализацией.

До SFINAE не было очевидной необходимости заботиться о тонких различиях между действиями, для которых Стандарт предписывал поведение, и теми, где все обычные реализации делали бы то же самое с мандатом или без него. Я не думаю, что со стороны Стандарта когда-либо было намерение поощрять или требовать реализации для обработки сдвигов чем-либо, кроме того, как это предписано C89, но, тем не менее, вещи, тем не менее, умудрялись обходить кусты, прежде чем они достигли точного результата. где они начали.

supercat 17.12.2018 20:19

Спасибо за очень интересный фон! Остается открытым вопрос о том, определяет ли gcc 255<<24 как расширение C++ 11.

Edgar Bonet 17.12.2018 22:06

CWG1457 - это DR.

T.C. 19.12.2018 06:36

@ T.C. Так что правильно будет сказать, что C++ 11 на самом деле тоже определяет это поведение?

Barry 19.12.2018 14:14

Да, я думаю, что это правильная интерпретация: режим C++ 11 гипотетического идеального компилятора должен рассматривать его как определено.

T.C. 20.12.2018 08:52

Компилятор GNU C полностью определяет сдвиги влево / вправо, как описано в руководство:

GCC supports only two’s complement integer types, and all bit patterns are ordinary values.

. . .

As an extension to the C language, GCC does not use the latitude given in C99 and C11 only to treat certain aspects of signed ‘<<’ as undefined. However, -fsanitize=shift (and -fsanitize=undefined) will diagnose such cases. They are also diagnosed where constant expressions are required.

Итак, это согласуется с вашими выводами - код делает то, что вы ожидаете, будет диагностировано только переполнение, превышающее доступные биты (включая бит знака).

Что касается компилятора GNU C++, документация кажется действительно не хватает. Мы можем только догадываться, что в случае отсутствия сдвига в G ++ работает так же, как и в GCC, хотя, по крайней мере, дезинфицирующее средство, похоже, знает о языковых различиях:

-fsanitize=shift

This option enables checking that the result of a shift operation is not undefined. Note that what exactly is considered undefined differs slightly between C and C++, as well as between ISO C90 and C99, etc.

Это довольно интересно, но я не уверен, что понимаю, какие именно аспекты << в руководстве называются «некоторыми аспектами». Жалко, что руководство по gcc не документирует реализацию C++ так подробно, как C.

Edgar Bonet 17.12.2018 22:03

Насколько я понимаю, GCC не придает особого значения знаковому биту. Таким образом, нет разницы между сдвигами со знаком и без знака.

rustyx 18.12.2018 11:01

Мне интересно, что gcc рассматривает постоянную поддержку поведения, которое было однозначно определено в C89, как «расширение», особенно с учетом того, что C99 Rationale не дает никаких указаний на то, что они намеревались предложить какие-либо изменения в том, как реализации двух дополнений без битов заполнения должен обрабатывать свое поведение.

supercat 19.12.2018 22:29

Другие вопросы по теме