Я столкнулся с интересным сценарием, в котором я получил разные результаты в зависимости от правильного типа операнда, и я не могу понять причину этого.
Вот минимальный код:
#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
) приведет к этим двум различным выходам?
Это действительно интересный случай. Это происходит только потому, что вы используете uint16_t
для типа без знака, когда вы используете архитектуру 32 бита для ìnt
Вот выдержка из Пункт 5 Выражения из проекта N4296 для C ++ 14 (подчеркните мой):
10 Многие бинарные операторы, которые ожидают операнды арифметического или перечислимого типа, вызывают преобразования …
Этот шаблон называется обычными арифметическими преобразованиями, которые определяются следующим образом:
…
(10.5.3) — Иначе, если операнд, имеющий целочисленный тип без знака имеет ранг больше или равен
ранг типа другого операнда, операнд со знаком целого типа должен быть преобразован в
тип операнда с целым типом без знака.
(10.5.4) — В противном случае, если тип операнда с целочисленный тип со знаком может представлять все значения
тип операнда с целым типом без знака, операнд с целым типом без знака должен
быть преобразован в тип операнда со знаком целочисленного типа.
Вы находитесь в случае 10.5.4:
uint16_t
только 16 бит int
это 32int
может представлять все значения 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
результат.
…
В противном случае, если E1 имеет тип со знаком и неотрицательное значение, а E1 × 2E2 является представима
в типе результата, тогда это итоговое значение; в противном случае поведение не определено.
Первое, что нужно понять, это то, что бинарные операторы, такие как 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, как известно, прямо исключает пути кода, которые неизбежно вызывают переполнение.
Давайте посмотрим на
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
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;
}
Затем ваши исходные 16 бит смещаются влево, что приводит к 32-битному значению с установленным старшим битом (0x80000000U), поэтому оно имеет отрицательное значение. Во время 64-битного преобразования происходит расширение знака, заполняющее верхние слова 1с.
Это результат целочисленного продвижения. Перед &
операция происходит, если операнды «меньше», чем 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
Ваша платформа имеет 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
показывая, где происходит продвижение и подписка-расширение.
& Операция имеет два операнда. Первый — это неподписанный шорт, который будет проходить обычные акции, чтобы стать int. Второй является константой, в одном случае типа int, в другом случае типа unsigned int. Результат & следовательно, int в одном случае, unsigned int в другом случае. Это значение сдвигается влево, что приводит либо к int с установленным битом знака, либо к unsigned int. Приведение отрицательного значения int к uint64_t даст большое отрицательное целое число.
Конечно, вы всегда должны следовать правилу: если вы что-то делаете и не понимаете результата, не делайте этого!