Подтвердить что ты не робот

Бит мудрый '&' с подписанным vs неподписанным операндом

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

Вот минимальный код:

#include <iostream>
#include <cstdint>

int main()
{
    uint16_t check = 0x8123U;

    uint64_t new_check = (check & 0xFFFF) << 16;

    std::cout << std::hex << new_check << std::endl;

    new_check = (check & 0xFFFFU) << 16;

    std::cout << std::hex << new_check << std::endl;

    return 0;
}

Я скомпилировал этот код с g++ (gcc версии 4.5.2) на Linux 64bit: g++ -std = С++ 0x -Wall example.cpp -o example

Выход был:

ffffffff81230000

81230000

Я не могу понять причину вывода в первом случае.

Почему в какой-то момент любой из результатов временного расчета будет продвигаться до знака подписанного 64-битного (int64_t), что приведет к расширению знака?

Я бы принял результат "0" в обоих случаях, если 16-битное значение сдвинуто на 16 бит, оставив в первую очередь, а затем продвинуто до 64-битного значения. Я также принимаю второй вывод, если компилятор сначала продвигает check в uint64_t, а затем выполняет другие операции.

Но как получилось, что & с 0xFFFF (int32_t) против 0xFFFFU (uint32_t) приведет к этим двум разным выходам?

4b9b3361

Ответ 1

Это действительно интересный угловой случай. Это происходит только потому, что вы используете uint16_t для неподписанного типа, когда в архитектуре используется 32 бита для ìnt

Вот выдержка из 5-го выражения из проекта n4296 для С++ 14 (подчеркните мое):

10 Многие двоичные операторы, ожидающие операндов арифметики или типа перечисления, вызывают конверсии... Этот шаблон называется обычным арифметическим преобразованием, которое определяется следующим образом:
...
(10.5.3) - В противном случае, если операнд с целым типом без знака имеет ранг, больший или равный ранга типа другого операнда, операнд со знаком целочисленного типа должен быть преобразован в тип операнда с целым типом без знака.
(10.5.4). В противном случае, если тип операнда с знаковым целым типом может представлять все значения тип операнда с целым типом без знака, операнд с целым числом без знака должен преобразуется в тип операнда со знаком целочисленного типа.

Вы находитесь в деле 10.5.4:

  • uint16_t - всего 16 бит, а int - 32
  • int может представлять все значения uint16_t

Итак, операнд uint16_t check = 0x8123U преобразуется в подписанный 0x8123, а результат побитового & по-прежнему равен 0x8123.

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

5.8 Операторы сдвига [expr.shift]
...
В противном случае, если E1 имеет подписанный тип и неотрицательное значение, а E1 × 2 E2 - представима в соответствующем неподписанном типе типа результата, то это значение, преобразованное в тип результата, является итоговое значение;...

и

4.7 Интегральные преобразования [conv.integral]
...
3 Если тип назначения подписан, значение не изменяется, если оно может быть представлено в типе назначения; в противном случае значение определяется реализацией.

(будьте осторожны, это было истинное поведение undefined в С++ 11...)

Итак, вы заканчиваете преобразованием подписанного int 0x81230000 в uint64_t, который, как и ожидалось, дает 0xFFFFFFFF81230000, потому что

4.7 Интегральные преобразования [conv.integral]
...
2 Если тип назначения не указан, результирующее значение представляет собой наименьшее целое число без знака, сравнимое с источником integer (по модулю 2n, где n - количество бит, используемых для представления неподписанного типа).

TL/DR: здесь не существует поведения undefined, в результате чего получается преобразование 32 битов int в unsigned 64 bits int. Единственной частью части, которая является поведение undefined, является сдвиг, который может вызвать переполнение знака, но все общие реализации разделяют это, и это реализация, определенная в стандарте С++ 14.

Конечно, если вы вынуждаете второй операнд быть беззнаковым, все будет беззнаковым, и вы получите, по-видимому, правильный результат 0x81230000.

[EDIT] Как объясняется MSalters, результатом сдвига является только реализация, определенная с С++ 14, но в действительности это поведение undefined в С++ 11. В пункте оператора смены говорится:

...
В противном случае, если E1 имеет подписанный тип и неотрицательное значение, а E1 × 2 E2 является представимым в типе результата, то это результирующее значение; , поведение undefined.

Ответ 2

Первое, что нужно понять, это то, что бинарные операторы типа a&b для встроенных типов работают только в том случае, если обе стороны имеют один и тот же тип. (С пользовательскими типами и перегрузками все идет). Это может быть реализовано посредством неявных преобразований.

Теперь в вашем случае определенно есть такое преобразование, потому что просто нет бинарного оператора &, который принимает тип меньше int. Обе стороны преобразуются как минимум в int, но какие именно типы?

Как это происходит, на вашем GCC int действительно 32 бит. Это важно, потому что это означает, что все значения uint16_t могут быть представлены как int. Нет переполнения.

Следовательно, check & 0xFFFF - простой случай. Правая сторона уже есть int, левая сторона продвигается до int, поэтому результат int(0x8123). Это прекрасно.

Теперь следующая операция 0x8123 << 16. Помните, что в вашей системе int есть 32 бита, а INT_MAX - 0x7FFF'FFFF. В отсутствие переполнения 0x8123 << 16 будет 0x81230000, но это явно больше, чем INT_MAX, поэтому на самом деле происходит переполнение.

Подписанное целочисленное переполнение в С++ 11 - это Undefined Поведение. Буквально любой результат правильный, включая purple или вообще не выводить. По крайней мере, вы получили числовое значение, но GCC, как известно, полностью устраняет пути кода, которые неизбежно вызывают переполнение.

[править] Новые версии GCC поддерживают С++ 14, где эта конкретная форма переполнения стала определяемой реализацией - см. Ответ Сержа.

Ответ 3

Посмотрим на

uint64_t new_check = (check & 0xFFFF) << 16;

Здесь 0xFFFF - подписанная константа, поэтому (check & 0xFFFF) дает нам целое число со знаком по правилам целочисленного продвижения.

В вашем случае с 32-разрядным типом int MSbit для этого целого после левого сдвига равен 1, и поэтому расширение до 64-разрядного без знака будет делать расширение знака, заполняя биты влево с помощью 1-х. Интерпретируется как представление с двумя дополнениями, которое дает такое же отрицательное значение.

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

Если ваша toolchain поддерживает __PRETTY_FUNCTION__, самую удобную функцию, вы можете быстро определить, как компилятор воспринимает типы выражений:

#include <iostream>
#include <cstdint>

template<typename T>
void typecheck(T const& t)
{
    std::cout << __PRETTY_FUNCTION__ << '\n';
    std::cout << t << '\n';
}
int main()
{
    uint16_t check = 0x8123U;

    typecheck(0xFFFF);
    typecheck(check & 0xFFFF);
    typecheck((check & 0xFFFF) << 16);

    typecheck(0xFFFFU);
    typecheck(check & 0xFFFFU);
    typecheck((check & 0xFFFFU) << 16);

    return 0;
}

Выход

void typecheck(const T &) [T = int]
65535
void typecheck(const T &) [T = int]
33059
void typecheck(const T &) [T = int]
-2128412672
void typecheck(const T &) [T = unsigned int]
65535
void typecheck(const T &) [T = unsigned int]
33059
void typecheck(const T &) [T = unsigned int]
2166554624

Ответ 4

0xFFFF - это подписанный int. Итак, после операции & у нас есть 32-разрядное знаковое значение:

#include <stdint.h>
#include <type_traits>

uint64_t foo(uint16_t a) {
  auto x = (a & 0xFFFF);
  static_assert(std::is_same<int32_t, decltype(x)>::value, "not an int32_t")
  static_assert(std::is_same<uint16_t, decltype(x)>::value, "not a uint16_t");
  return x;
}

http://ideone.com/tEQmbP

Ваши исходные 16 бит затем сдвигаются влево, что приводит к 32-битовому значению с высоким набором бит (0x80000000U), поэтому оно имеет отрицательное значение. Во время 64-битного преобразования происходит расширение знака, заполняя верхние слова 1 сек.

Ответ 5

Это результат целостного продвижения. Перед выполнением операции &, если операнды "меньше", чем int (для этой архитектуры), компилятор будет поддерживать оба операнда до int, потому что они оба вписываются в signed int:

Это означает, что первое выражение будет эквивалентно (в 32-битной архитектуре):

// check is uint16_t, but it fits into int32_t.
// the constant is signed, so it sign-extended into an int
((int32_t)check & (int32_t)0xFFFFFFFF)

в то время как другой будет иметь второй операнд, продвигаемый в:

// check is uint16_t, but it fits into int32_t.
// the constant is unsigned, so the upper 16 bits are zero
((int32_t)check & (int32_t)0x0000FFFFU)

Если вы явно введете check в unsigned int, тогда результат будет одинаковым в обоих случаях (unsigned * signed приведет к unsigned):

((uint32_t)check & 0xFFFF) << 16

будет равно:

((uint32_t)check & 0xFFFFU) << 16

Ответ 6

Ваша платформа имеет 32-разрядный int.

Ваш код в точности эквивалентен

#include <iostream>
#include <cstdint>

int main()
{
    uint16_t check = 0x8123U;
    auto a1 = (check & 0xFFFF) << 16
    uint64_t new_check = a1;
    std::cout << std::hex << new_check << std::endl;

    auto a2 = (check & 0xFFFFU) << 16;
    new_check = a2;
    std::cout << std::hex << new_check << std::endl;
    return 0;
}

Какой тип a1 и a2?

  • Для a2 результат повышается до unsigned int.
  • Более интересно, для a1 результат продвигается до int, а затем он расширяется по знаку, расширяя его до uint64_t.

Здесь более короткая демонстрация, в десятичной форме, так что разница между подписанными и неподписанными типами очевидна:

#include <iostream>
#include <cstdint>

int main()
{
    uint16_t check = 0;
    std::cout << check
              << "  " << (int)(check + 0x80000000)
              << "  " << (uint64_t)(int)(check + 0x80000000) << std::endl;
    return 0;
}

В моей системе (также 32-бит int) я получаю

0  -2147483648  18446744071562067968

показывает, где происходит продвижение и расширение подписки.

Ответ 7

Операция и операция имеют два операнда. Первый - это беззнаковый короткий, который будет проходить обычные акции, чтобы стать int. Вторая - константа, в одном случае типа int, в другом случае типа unsigned int. Результат и, следовательно, int в одном случае, unsigned int в другом случае. Это значение смещается влево, в результате получается либо int с установленным битом знака, либо беззнаковым int. Отбрасывание отрицательного int в uint64_t даст большое отрицательное целое число.

Конечно, вы всегда должны следовать правилу: если вы что-то делаете, и вы не понимаете результата, то не делайте этого!