Немного мудрый ‘& amp;’ с подписанным против неподписанного операнда

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

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

#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 = c ++ 0x -Wall example.cpp -o пример

Выход был:

ffffffff81230000

81230000

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

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

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

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

28

Решение

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

Вот выдержка из Пункт 5 Выражения из проекта N4296 для C ++ 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 × 2E2 является представима
в соответствующем беззнаковом виде
типа результата, то это значение, преобразованное в тип результата, является
итоговое значение; …

а также

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

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

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

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

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

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

[РЕДАКТИРОВАТЬ] Как объяснил MSalters, результат сдвига только реализация определена начиная с C ++ 14, но был действительно неопределенное поведение в C ++ 11. В пункте оператора сдвига сказано:


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

21

Другие решения

Первое, что нужно понять, это то, что бинарные операторы, такие как 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 так что на самом деле переполнение.

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

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

10

Давайте посмотрим на

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

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

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

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

Если ваш инструментарий поддерживает __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
9

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с.

2

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

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

// check is uint16_t, but it fits into int32_t.
// the constant is signed, so it's 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
1

Ваша платформа имеет 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

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

1

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

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

0
По вопросам рекламы [email protected]