Перевернуть наименее значимый бит, используя отрицание size_t

#c #bit-manipulation #bit

#c #манипулирование битами #бит

Вопрос:

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

 size_t a = 19;
std::cout<<std::bitset<8>(a)<<std::endl;
a ^= a amp; -a;
std::cout<<std::bitset<8>(a)<<std::endl;
  

Этот фрагмент кода инвертирует младший значащий бит заданного целого числа без знака. Я бы предпочел просто написать a ^= 1; , но я озадачен тем, почему приведенный выше фрагмент кода действительно работает. Я бы подумал, что unsigned int отрицательное значение приведет к неопределенному поведению?

Комментарии:

1. Осторожно: он переворачивает наименее значимый установленный бит (первый установленный бит снизу), а не младший значащий бит (который может быть 0 или 1). Этот код не эквивалентен a ^= 1; (но он эквивалентен a amp;= a - 1;

Ответ №1:

a amp; -a дает вам наименьший значимый 1-битный набор в a . Для нечетного числа это действительно 1, но, конечно, в целом это не так.

Создание unsigned отрицательного значения является четко определенным и иногда полезным обозначением: -a для положительного a значения является -a 2N, где N — количество битов в типе. Альтернативой записи size_t a = std::numeric_limits<size_t>::max(); является, например, запись size_t a = -1; .

Таким образом, a ^= a amp; -a; переворачивает наименее значимый бит 1 в 0.

На самом деле довольно умно.

Комментарии:

1. Смотрите также [conv.integral]/p2

Ответ №2:

Как уже указывала @Bathsheba, этот трюк дает вам наименьший 1-битный набор в a. Однако я хотел бы более подробно рассказать, почему это происходит. Отрицание целого числа без знака в C эквивалентно отрицанию дополнения two:

Унарные арифметические операторы

[…]
Встроенный унарный оператор minus вычисляет отрицание своего продвинутого операнда. Для неподписанного a значение -a равно 2b -a, где b — количество бит после продвижения.

(см. cppreference / Арифметические операторы)

Для чисел дополнения two отрицание может быть выполнено следующим образом:

 unsigned a = ...;
a = ~a;
a  = 1;
  

Если бы не приращение, то у ~a не было бы общих битов с a , и результат был бы нулевым. Это относится к числу дополнений. Однако из-за приращения последний значимый набор 1-бит в a также становится установленным. Например:

  16      = 0b0001'0000
~16      = 0b1110'1111 = -17
~16   1  = 0b1111'0000 = -16
-16 amp; 16 = 0b0001'0000 =  16

 10      = 0b0000'1010
~10      = 0b1111'0101 = -11
~10   1  = 0b1111'0110 = -10
-10 amp; 10 = 0b0000'0010 =   2
  

a ^= a amp; -a затем переворачивает наименее значимый бит 1 в 0.
Что это математически делает, так это:

  • округлите до следующего кратного степени двойки
  • преобразуйте любую степень 2 в 0
  • 0 остается неизменным

Также обратите внимание, что начиная с C 20, числа со знаком должны быть представлены с использованием дополнения two. Например, это означает, что переполнение целого числа со знаком больше не является неопределенным поведением.

Диапазон значений

[…]

До C 20 стандарт C допускал любое представление целых чисел со знаком, а минимальный гарантированный диапазон N-разрядных целых чисел со знаком составлял от — (2N-1-1) до 2N-1-1 (например, от -127 до 127 для 8-разрядного типа со знаком), что соответствует пределам дополнения или знака и величины.

Однако все компиляторы C используют представление с дополнением two, и начиная с C 20 это единственное представление, разрешенное стандартом, с гарантированным диапазоном от -2N-1 до 2N-1 -1 (например, от -128 до 127 для 8-разрядного типа со знаком).

(См. cppreference/ Фундаментальные типы)