#cpp #language_lawyer
В стандарте языка неоднократно упоминается, что операции с беззнаковыми целыми выполняются по модулю 2^n, где n - количество бит, участвующих в представлении значения беззнакового целого. Пусть есть такой код: unsigned char a = 1; a = -a; И, пусть unsigned char восьмибитный, а int шестнадцатибитный. Я уже было подумал, что выражение -a либо сразу преобразуется в значение типа unsigned char, равное 255; либо a расширится до int, затем этот int примет значение -1 и, наконец, -1 преобразуется в значение типа unsigned char, равное 255. Однако, как оказалось, в стандарте указан особый, довольно странный способ вычисления унарного минуса для беззнаковых величин. Вот цитата пункта 8.3.1/8 из этого документа: The operand of the unary - operator shall have arithmetic or unscoped enumeration type and the result is the negation of its operand. Integral promotion is performed on integral or enumeration operands. The negative of an unsigned quantity is computed by subtracting its value from 2^n, where n is the number of bits in the promoted operand. The type of the result is the type of the promoted operand. Если я правильно понял написанное, то значение выражения -a в приведённом выше примере равняется величине 2^n - 1, где n равно количеству бит в расширенном операнде. Т.е. 2^n - 1 == 2^16 - 1 == 65536 - 1 == 65535. Но данное значение не может быть представлено в шестнадцатибитном int, а значит попытка вычислить -a приводит к неопределённому поведению. Вопрос состоит в следующем: может ли унарный минус "работать" так, как написано выше? И если да, то почему стандарт определяет такое странное поведение унарного минуса для беззнаковых величин? Странно ещё то, что если написать так: unsigned char a = 1; a = 0 - a; то это вроде как не может привести к неопределённому поведению. Так как 0 имеет тип int, переменная a расширится до int, вычислится разность 0-1, и получившееся в результате значение типа int благополучно усечётся по модулю 256 до значения 255.
Ответы
Ответ 1
Слова про 2n относятся только к unsigned типам (арифметика по модулю max() + 1 — результат всегда определён). Эти слова не относятся к int типу. Если все значения unsigned char типа помещаются в int (часто бывает), то (C++ n4659 §7.6.1): -(unsigned char)1 = (integer promotion, likely) = -(int)1 = (int)-1 Если не помещаются, то: -(unsigned char)1 = (integer promotion, rare) = -(unsigned int)1 = (unsigned int)2**n-1 = (by definition) = (unsigned int)UINT_MAX где n это кол-во бит для значений (value bits — могут отличаться от sizeof * CHAR_BIT, если есть padding bits — C n1570 §6.2.6.2.1). То есть тип -a выражения равен int или unsigned int. Чтобы присвоить назад в a необходимо (возможно сужающее преобразование) в unsigned char: (int)-1 = (UCHAR_MAX + 1) - 1 = (unsigned char)UCHAR_MAX или: (unsigned int)UINT_MAX = UINT_MAX % (UCHAR_MAX + 1) = (unsigned char)UCHAR_MAX то есть значение всегда получается в этом случае 2CHAR_BIT-1: a == std::numeric_limits::max() Ответ 2
Определимся с понятиями в документе: п. 6.10: A prvalue is an expression whose evaluation initializes an object or a bit-field, or computes the value of the operand of an operator, as specified by the context in which it appears п. 7.6 Integral promotions A prvalue of an integer type other than bool, char16_t, char32_t, or wchar_t whose integer conversion rank (7.15) is less than the rank of int can be converted to a prvalue of type int if int can represent all the values of the source type; otherwise, the source prvalue can be converted to a prvalue of type unsigned int Суть всего этого в том, что для сохранения всего диапазона значений после вычисления, например унарного минуса, может применяться обобщающее расширение типа до int. Необходимость этого действия (promoted operand) определяется контекстом. В коде unsigned char a = 10; unsigned char b = -a; обобщающее расширение можно не применять, так как и результат и место его хранения - один байт, дополнительного места для хранения не требуется. Здесь N=8, 2^8=256, 256-10=246. А вот здесь int c = -a; тип для сохранения результата int, поэтому N=32, 2^32-10=-10 (в дополнительном коде). В 8.3.1 Unary operators п.8 утверждается, что величина N должна быть взята не от исходного типа, а от конечного, который, в частности, может быть обобщающим - всё исходя из контекста. А грабли лежат совсем рядом: 7.8 Integral conversions If the destination type is signed, the value is unchanged if it can be represented in the destination type; otherwise, the value is implementation-defined. 7.9 Floating-point conversions A prvalue of floating-point type can be converted to a prvalue of another floating-point type. If the source value can be exactly represented in the destination type, the result of the conversion is that exact representation. If the source value is between two adjacent destination values, the result of the conversion is an implementation-defined choice of either of those values. Otherwise, the behavior is undefined Поэтому предупреждения компилятора о попытке сравнить знаковый тип с беззнаковым или о неявных преобразованиях с понижением размерности пренебрегать не стоит.
Комментариев нет:
Отправить комментарий