Что происходит с поразрядными операторами и целочисленным продвижением?

У меня простая программа. Обратите внимание, что я использую беззнаковое целое число фиксированной ширины размером 1 байт.

#include <cstdint>
#include <iostream>
#include <limits>

int main()
{
    uint8_t x = 12;
    std::cout << (x << 1) << '\n';
    std::cout << ~x;

    std::cin.clear();
    std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
    std::cin.get();

    return 0;
}

Мой вывод следующий.

24
-13

Я тестировал большие числа, и оператор << всегда дает мне положительные числа, а оператор ~ всегда дает мне отрицательные числа. Затем я использовал sizeof() и обнаружил ...

Когда я использую побитовый оператор сдвига влево (<<), я получаю 4-байтовое целое число без знака.

Когда я использую побитовый оператор not (~), я получаю 4-байтовое целое число со знаком.

Кажется, что побитовый оператор not (~) выполняет целочисленное продвижение со знаком, как это делают арифметические операторы. Однако оператор сдвига влево (<<), похоже, способствует беззнаковому интегралу.

Я чувствую себя обязанным знать, когда компилятор что-то меняет за моей спиной. Если я прав в своем анализе, все ли побитовые операторы превращаются в 4-байтовое целое число? И почему некоторые подписаны, а некоторые нет? Я весьма озадачен!

Изменить: Мое предположение о том, что всегда получаются положительные или всегда отрицательные значения, было неверным. Но, будучи неправым, я понимаю, что на самом деле происходило, благодаря отличным ответам, приведенным ниже.


person Wandering Fool    schedule 27.05.2015    source источник
comment
Как у вас есть потоки для вывода вашего uint8_t в виде числа, а не символа? Вы уверены, что ваш компилятор не присваивает этому типу псевдоним int?   -  person Anton Samsonov    schedule 27.05.2015
comment
@AntonSamsonov В ответе ниже он объясняет это как результат интегрального продвижения, которое происходит после побитовой операции. Другими словами, тип данных был повышен с uint8_t до int.   -  person Wandering Fool    schedule 27.05.2015


Ответы (4)


[expr.unary.op]

Операнд ~ должен иметь целочисленный или незаданный тип перечисления; результатом является единичное дополнение своего операнда. Выполняются комплексные рекламные акции.

[expr.shift]

Операторы сдвига << и >> группируют слева направо. [...] Операнды должны быть целочисленного типа или типа перечисления без области действия, и выполняются целые рекламные акции.

Что такое интегральное продвижение uint8_t (которое обычно будет unsigned_char за кулисами)?

[conv.prom]

Prvalue целочисленного типа, отличного от bool, char16_t, char32_t или wchar_t, чей ранг целочисленного преобразования (4.13) меньше ранга int, может быть преобразовано в prvalue типа int, если int может представлять все значения исходного типа. ; в противном случае исходное prvalue может быть преобразовано в prvalue типа unsigned int.

Итак, int, потому что все значения uint8_t могут быть представлены int.

Что такое int(12) << 1? int(24).

Что такое ~int(12)? int(-13).

person user657267    schedule 27.05.2015
comment
И причина, по которой я получал все отрицательные числа после использования побитового оператора not (~), заключается в том, что большинство двоичных чисел имеют завершающие 0 с левой стороны, и когда они переворачиваются, самая левая цифра, вероятно, будет равна 1, что делает число отрицательным. . Это особенно верно, если память, содержащая значение, имеет размер 4 байта, что дает мне 2 ^ 32 возможных значения, и если значение, которое я выбираю, намного меньше этого диапазона. - person Wandering Fool; 27.05.2015
comment
Похоже, вы нашли эту информацию о компиляторе из подробной книги или руководства по C ++. Если да, то как это называется? Я хотел бы использовать его как ресурс, если я застряну в работе компилятора. - person Wandering Fool; 27.05.2015
comment
@WanderingIdiot, идущий слева, называется ведущим. Книга называется стандартом C ++ (не рекомендуемым ресурсом). en.cppreference.com/w/cpp/language/ - person Marc Glisse; 27.05.2015
comment
Но uint8_t byte1, byte2; cout >> sizeof(byte1 & byte2); Возврат 4 - это для меня провал в языке! - person DrumM; 19.03.2019
comment
@DrumM byte1 & byte2 повысился до int, чего вы ожидали? - person user657267; 22.03.2019
comment
Поскольку оба они имеют размер 1 байт, я ожидаю, что результат операции И также будет 1 байт! А что насчет uint64_t? Поскольку это больше, чем int, результатом операции И для 2 uint64_ts будет uint64_t. Это показывает, что в языке есть изъян, он совсем не согласован. - person DrumM; 27.03.2019
comment
@DrumM Это не недостаток как таковой, а унаследованная причуда, оставшаяся от C; Я думаю, добро пожаловать в C ++. Изменение правил сейчас нарушит непостижимое количество унаследованного кода, так что вам просто придется иметь дело с ним, как и со всеми остальными. - person user657267; 27.03.2019
comment
Да, правильно, просто сложно самостоятельно выяснить все эти причуды, если у вас есть страница со ВСЕМИ этими причудами, дайте мне знать ;-) Спасибо! - person DrumM; 28.03.2019

По соображениям производительности языки C и C ++ считают int "наиболее естественным" целочисленным типом, и вместо этого типы, которые "меньше", чем int, считаются своего рода типом "хранилища".

Когда вы используете тип хранилища в выражении, он автоматически преобразуется в int или в unsigned int неявно. Например:

// Assume a char is 8 bit
unsigned char x = 255;
unsigned char one = 1;

int y = x + one; // result will be 256 (too large for a byte!)
++x;             // x is now 0

произошло то, что x и one в первом выражении были неявно преобразованы в целые числа, сложение было вычислено, а результат был сохранен обратно в целое число. Другими словами, вычисление НЕ было выполнено с использованием двух беззнаковых символов.

Точно так же, если у вас есть значение float в выражении, первое, что сделает компилятор, это повысит его до double (другими словами, float - это тип хранилища, а double - это естественный размер для чисел с плавающей запятой). Это причина, по которой, если вы используете printf для печати чисел с плавающей запятой, вам не нужно указывать %lf int строки формата, и %f достаточно (%lf требуется для scanf, однако, потому что эта функция сохраняет результат и float может быть меньше double).

C ++ немного усложнил дело, потому что при передаче параметров функциям вы можете различать int и более мелкие типы. Таким образом, не ВСЕГДА верно, что преобразование выполняется в каждом выражении ... например, вы можете иметь:

void foo(unsigned char x);
void foo(int x);

где

unsigned char x = 255, one = 1;
foo(x);       // Calls foo(unsigned char), no promotion
foo(x + one); // Calls foo(int), promotion of both x and one to int
person 6502    schedule 27.05.2015
comment
Мне понравилось ваше последнее замечание о том, что неявные преобразования не выполняются при передаче параметров функции. Это полезная информация, спасибо. - person Wandering Fool; 27.05.2015
comment
Что в этом такого естественного? Что ж, компилятор может делать все, что ему нужно / хочет вычислить результат (максимально быстрым способом или с учетом других соображений), но он должен сохранять (самый большой) тип операнда. Только программист должен расширять тип выражения - с явным приведением; любое другое поведение противоречит интуиции. Или, с вашей точки зрения, более естественно написать явное приведение всех выражений, чтобы вернуть их в исходную область? - person Anton Samsonov; 27.05.2015
comment
Точно так же, если у вас есть значение с плавающей запятой в выражении, первое, что сделает компилятор, - это повысит его до двойного - это не так в C ++. Это происходит только как часть продвижений аргументов по умолчанию (параграф [5.2.2p7] в стандарте), которые применяются только к аргументам функции, которые совпадают с спецификацией параметра многоточия (..., поэтому это происходит для printf). В a + b, если и a, и b равны float, ни один из них не повышается и тип результата - float; если один float, а другой double, то преобразование происходит, но это что-то другое. - person bogdan; 27.05.2015
comment
@bogdan: функция, выполняющая a = b + c, где a, b и c равны float, генерирует такой же идентичный байтовый машинный код, что и выполняющая вместо a = (double)b + (double)c. - person 6502; 27.05.2015
comment
Это деталь реализации, и это верно даже не для всех реализаций. Я только что убедился, что MSVC12 генерирует очень разный код для ваших двух случаев. Он также генерирует предупреждение о преобразовании из double в float для вашего второго случая (на /W4), чего не происходит в первом случае. Вы можете проверить, что b + c имеет тип float, используя std::is_same<decltype(a + b), float>::value. - person bogdan; 27.05.2015
comment
Причина, по которой компиляторам разрешено делать то, что вы сказали, заключается в параграфе [5p12]: значения плавающих операндов и результаты плавающих выражений могут быть представлены с большей точностью и диапазоном, чем требуется типом; типы при этом не меняются. (курсив мой). Это сильно отличается от правил для обычных арифметических преобразований и целочисленных повышений, которые фактически изменяют типы операндов и результатов. - person bogdan; 27.05.2015
comment
@bogdan: приятно знать, исправлено - person 6502; 27.05.2015

Я тестировал большие числа, и оператор ‹< всегда дает мне положительные числа, а оператор ~ всегда дает мне отрицательные числа. Затем я использовал sizeof () и обнаружил ...

Неправильно, проверьте это:

uint8_t v = 1;
for (int i=0; i<32; i++) cout << (v<<i) << endl;

дает:

1
2
4
8
16
32
64
128
256
512
1024
2048
4096
8192
16384
32768
65536
131072
262144
524288
1048576
2097152
4194304
8388608
16777216
33554432
67108864
134217728
268435456
536870912
1073741824
-2147483648

uint8_t - это 8-битный беззнаковый целочисленный тип, который может представлять значения в диапазоне [0,255], поскольку этот диапазон включен в диапазон int, он повышается до int (не unsigned int). Повышение до int имеет приоритет перед повышением до unsigned.

person Jean-Baptiste Yunès    schedule 27.05.2015

Изучите дополнение до двух и то, как компьютер хранит отрицательные целые числа.
Попробуйте это

#include <cstdint>
#include <iostream>
#include <limits>
int main()
{
uint8_t x = 1;
int shiftby=0;
shiftby=8*sizeof(int)-1;
std::cout << (x << shiftby) << '\n'; // or std::cout << (x << 31) << '\n';

std::cout << ~x;

std::cin.clear();
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
std::cin.get();
return 0;
}

Результат -2147483648

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

** EDIT **
Ну, я могу придумать причину, по которой операторы сдвига будут использовать unsigned int. Рассмотрим операцию сдвига вправо >>, если сдвинуть вправо -12, вы получите 122 вместо -6. Это потому, что он добавляет ноль в начале без учета знака

person dev_ankit    schedule 27.05.2015
comment
Я не верю, что это полностью отвечает на мой вопрос, но позвольте мне следить за тем, что вы говорите, и, возможно, я разберусь с этим. Когда вы вышли из смены 31 раз, значение переполняется. Исходя из этого, похоже, что оператор сдвига влево выполняет целое продвижение со знаком вместо целого продвижения без знака. Так что, если это правда, то это будет означать, что компилятор проверяет крайнюю левую цифру двоичного числа, чтобы определить ее знак. Это должно быть так! Компилятор всегда должен выполнять интегральное продвижение с поразрядными операторами, когда операнд уже int. - person Wandering Fool; 27.05.2015
comment
Чего ждать? Я не знаю, какой компилятор вы использовали для проверки, но GCC на x86_64 выдает инструкцию SAR для сдвига со знаком, которая сохраняет знак. Следовательно, (-12 ›› 1) == -6. - person Arne Vogel; 27.05.2015