Обязательны ли битовые маски для беззнаковых преобразований?

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

В частности, я хотел бы знать, ожидает ли стандарт C, что беззнаковые типы, преобразованные в меньшие беззнаковые типы, просто потеряют свои самые значимые биты без использования какой-либо битовой маски.

Пример: 0xABC (16 бит) -> 0xBC (8 бит).

Пример кода (Общая ссылка):

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

void print_small_hex_value(uint8_t value) {
    printf("Small hex value from function: %llx\n", value);
}

int main()
{
    uint64_t large_value = 0xABCDEFABCDEFABCD;
    printf("Large hex value: %llx\n", large_value);
    uint8_t small_value = large_value; /* without bit mask */
    printf("Small hex value: %llx\n", small_value);
    uint8_t small_value_masked = large_value & 0xFF; /* with bit mask */
    printf("Small hex value masked: %llx\n", small_value);
    printf("\n");
    print_small_hex_value(large_value); /* print from function */
    print_small_hex_value(large_value & 0xFF);
    print_small_hex_value(small_value);
}

Выход:

Large hex value: abcdefabcdefabcd
Small hex value: cd
Small hex value masked: cd

Small hex value from function: cd
Small hex value from function: cd
Small hex value from function: cd

Мне кажется, «магическое» преобразование работает и без битовых масок.

Итак, почему многие кодовые базы (например, CPython) принудительно используют битовую маскировку (т. е. value & 0xFF)? Просто позже компиляторы исключают это, если в этом нет необходимости? Я только не заметил, что в этих случаях вы действительно имеете дело со знаковыми целыми числами?

Какая разница, если большее значение (т. е. uint64_t) передается как параметр uint8_t или сохраняется в переменной uint8_t? Рассматриваются ли составители по-разному в этих двух случаях?

Может ли кто-нибудь указать надежный источник по этому вопросу (например, стандарт C)?

why many codebases (i.e. CPython) force the bits through bit masking (a.k.a. value & 0xFF)?Общего ответа нет. Чтобы ответить на этот вопрос, вам нужно опубликовать конкретный код.
KamilCuk 18.04.2024 11:54

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

interjay 18.04.2024 11:59

Из n1124 раздела 6.2.5 о типах: .... Вычисление с участием беззнаковых операндов никогда не может переполниться, поскольку результат, который не может быть представлен результирующим целочисленным типом без знака, уменьшается по модулю числа, которое на единицу больше самого большого значение, которое может быть представлено результирующим типом.

Ghorban M. Tavakoly 18.04.2024 12:21

@KamilCuk, например, в github.com/python/cpython/blob/main/Objects/floatobject.c мне кажется, что это И беззнаковый символ с 0xFF.

Vincenzo Maggio 18.04.2024 12:30

Линия 2193? Технически unsigned char может иметь более 8 бит. Но я бы сказал, что это сделано для удобства чтения.

KamilCuk 18.04.2024 12:39

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

Lundin 18.04.2024 13:19

Я не могу найти место, где бы беззнаковый символ использовался таким образом в этих источниках Python. Хотите предоставить очередь?

Ulrich Eckhardt 18.04.2024 13:30
Стоит ли изучать 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 называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
4
7
75
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Стандарт C ожидает, что беззнаковые типы, преобразованные в меньшие беззнаковые типы, просто потеряют свои самые значимые биты без использования какой-либо битовой маски.

Да.

Линия:

%llx\n", small_value

и подобные другие недействительны. См. https://godbolt.org/z/b7xa794x1 . %llx ожидает unsigned long long спора. small_value имеет тип uint8_t. Вы должны использовать PRIx8, чтобы inttypes.h распечатать его.

Просто позже компиляторы исключают это, если в этом нет необходимости?

В целом да.

Только я не заметил, что в этих случаях вы действительно имеете дело со знаковыми целыми числами?

Нет.

Какая разница, если большее значение (т. е. uint64_t) передается как параметр uint8_t или сохраняется в переменной uint8_t?

Нет разницы.

Рассматриваются ли составители по-разному в этих двух случаях?

За исключением очевидного, нет.

Может ли кто-нибудь указать на надежный источник по этому вопросу (например, стандарт C)?

Когда значение присваивается переменной определенного типа, это значение преобразуется в целевой тип. Пока можете прочитать https://port70.net/~nsz/c/c11/n1570.html#6.3.1.3p2 :

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

0xABCDEFABCDEFABCD — это 12379814471884843981. Мы многократно вычитаем 256 из этого числа 48358650280800171 раз. После этой операции у нас осталось 205, что в шестнадцатеричном формате равно 0xCD. По сути, это причудливый способ описания & 0xff.

Сегодня у нас есть более понятный cppreference https://en.cppreference.com/w/c/language/conversion .

почему многие кодовые базы (например, CPython) заставляют биты использовать маскирование битов (также известное как значение и 0xFF)?

Это может быть предпочтение программиста по удобству чтения или сопровождению. В C также есть стандарты безопасности, например, правило 10.3 MISRA 2012 требует, чтобы вы писали uint8_t small_value = (uint8_t)large_value;, но я не думаю, что знаю правило, которое требовало бы маскировки.

Действительно ясно и исчерпывающе! Спасибо также за ссылку на стандарты безопасности в C.

Vincenzo Maggio 18.04.2024 12:32

Что касается доверенных источников/стандартной части C:

Как мы узнаем, что обращение произошло?

В вашем примере вы не вызывали явное преобразование с помощью приведения, вы просто написали uint8_t small_value = large_value;. Итак, здесь произошло неявное преобразование — как мы можем знать, что это произойдет? Это оператор присваивания, поэтому нам нужно разобраться с его правилами. С17 6.5.16.1:

Тип выражение присваивания — это тип, который будет иметь левый операнд после преобразования lvalue.
/--/
При простом присваивании (=) значение правого операнда преобразуется в тип выражения присваивания и заменяет значение, хранящееся в объекте, обозначенном левым операндом.

Ладно, это не очень полезно. Мы можем сказать, что произойдет преобразование, и что преобразование произойдет «к тому типу, который левый операнд будет иметь после преобразования lvalue». Хорошо, теперь мы найдём преобразования lvalue, C17 6.3.2.1:

Lvalue — это выражение (с типом объекта, отличным от void), которое потенциально обозначает объект
/--/
«За исключением случаев...» (длинный список исключений здесь)
«...lvalue, не имеющее типа массива, преобразуется в значение, хранящееся в назначенном объекте (и больше не является lvalue); это называется преобразованием lvalue. Если lvalue имеет уточненный тип, значение имеет неполную версию типа lvalue».

Хорошо, это просто тарабарщина для всех, кроме «юристов языка C». На простом английском языке это означает, что во время присваивания правый операнд преобразуется в тип левого операнда, и если правый операнд оказался чем-то вроде const int («полный тип»), то const отбрасывается (из правого операнда). операнд =) перед преобразованием.

Итак, в этом случае uint64_t неявно преобразуется в uint8_t, что гарантируется правилами оператора присваивания.


Каковы правила фактического преобразования?

Целочисленные преобразования из любого целочисленного типа в беззнаковый тип всегда четко определены:

С17 6.3.1.3

В противном случае, если новый тип не имеет знака, значение преобразуется путем многократного добавления или вычитания на единицу больше максимального значения, которое может быть представлено в новом типе, пока значение не окажется в диапазоне нового типа. 60)

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

Так что это правило работает «как по модулю». Если у нас есть целое число со значением 0xABBA и преобразуем его в uint8_t, то:

  • На единицу больше максимального значения uint8_t256.
  • Если мы будем постоянно математически вычитать 256 из 0xABBA, первое число, которое мы получим в диапазоне значений 0-255 нового типа uint8_t, будет 186, 0xBA.
  • Это то же самое, что и 0xABBA % 256, следовательно, «как если бы модуль».
  • Мы также можем отметить, что в шестнадцатеричной записи это то же самое, что отбросить все старшие байты, которые не поместились.

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