Я реализую игрушечный проект по изучению 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)?
Маска не нужна при преобразовании в меньший беззнаковый тип, но некоторые базы кода могут включать ее, чтобы сделать ее явной.
Из n1124 раздела 6.2.5 о типах: .... Вычисление с участием беззнаковых операндов никогда не может переполниться, поскольку результат, который не может быть представлен результирующим целочисленным типом без знака, уменьшается по модулю числа, которое на единицу больше самого большого значение, которое может быть представлено результирующим типом.
@KamilCuk, например, в github.com/python/cpython/blob/main/Objects/floatobject.c мне кажется, что это И беззнаковый символ с 0xFF.
Линия 2193? Технически unsigned char
может иметь более 8 бит. Но я бы сказал, что это сделано для удобства чтения.
Примечательно, что в C маскирование является хорошей практикой, поскольку компиляторы часто предостерегают от «сужающего преобразования», а маскирование имеет тенденцию закрывать такие предупреждения.
Я не могу найти место, где бы беззнаковый символ использовался таким образом в этих источниках Python. Хотите предоставить очередь?
Стандарт 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.
Что касается доверенных источников/стандартной части 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)
- Правила описывают арифметику математического значения, а не значения выражения данного типа.
Так что это правило работает «как по модулю». Если у нас есть целое число со значением 0xABBA и преобразуем его в uint8_t
, то:
uint8_t
— 256
.256
из 0xABBA, первое число, которое мы получим в диапазоне значений 0-255 нового типа uint8_t
, будет 186, 0xBA.
why many codebases (i.e. CPython) force the bits through bit masking (a.k.a. value & 0xFF)?
Общего ответа нет. Чтобы ответить на этот вопрос, вам нужно опубликовать конкретный код.