Как работают правила продвижения, когда подпись с обеих сторон двоичного оператора различна?

Рассмотрим следующие программы:

// http://ideone.com/4I0dT
#include <limits>
#include <iostream>

int main()
{
    int max = std::numeric_limits<int>::max();
    unsigned int one = 1;
    unsigned int result = max + one;
    std::cout << result;
}

а также

// http://ideone.com/UBuFZ
#include <limits>
#include <iostream>

int main()
{
    unsigned int us = 42;
    int neg = -43;
    int result = us + neg;
    std::cout << result;
}

Как оператор + "знает", какой тип возвращать? Общее правило состоит в том, чтобы преобразовать все аргументы в самый широкий тип, но здесь нет четкого "победителя" между int а также unsigned int, В первом случае unsigned int должен быть выбран в результате operator+потому что я получаю результат 2147483648, Во втором случае нужно выбирать intпотому что я получаю результат -1, Тем не менее, я не вижу в общем случае, как это разрешимо. Это неопределенное поведение, которое я вижу, или что-то еще?

3 ответа

Решение

Это четко указано в §5/9:

Многие бинарные операторы, которые ожидают операнды арифметического или перечислимого типа, вызывают преобразования и выдают типы результатов аналогичным образом. Цель состоит в том, чтобы получить общий тип, который также является типом результата. Этот шаблон называется обычными арифметическими преобразованиями, которые определяются следующим образом:

  • Если один из операндов имеет тип long doubleдругой должен быть преобразован в long double,
  • В противном случае, если любой из операндов doubleдругой должен быть преобразован в double,
  • В противном случае, если любой из операндов floatдругой должен быть преобразован в float,
  • В противном случае интегральные продвижения должны выполняться для обоих операндов.
  • Затем, если любой из операндов unsigned long другой должен быть преобразован в unsigned long,
  • В противном случае, если один операнд long int и другие unsigned intтогда, если long int может представлять все значения unsigned int, unsigned int должны быть преобразованы в long int; в противном случае оба операнда должны быть преобразованы в unsigned long int,
  • В противном случае, если любой из операндов longдругой должен быть преобразован в long,
  • В противном случае, если любой из операндов unsignedдругой должен быть преобразован в unsigned,

[Примечание: в противном случае единственным оставшимся случаем является то, что оба операнда int]

В обоих ваших сценариях результат operator+ является unsigned, Следовательно, второй сценарий эффективно:

int result = static_cast<int>(us + static_cast<unsigned>(neg));

Потому что в этом случае значение us + neg не может быть представлен int, значение result определяется реализацией - §4.7/3:

Если тип назначения подписан, значение не изменяется, если оно может быть представлено в типе назначения (и ширине битового поля); в противном случае значение определяется реализацией.

До стандартизации C существовали различия между компиляторами - некоторые следовали правилам "сохранения значений", а другие - "сохранению знака". Сохранение знака означало, что если любой операнд был беззнаковым, результат был беззнаковым. Это было просто, но иногда давало довольно удивительные результаты (особенно, когда отрицательное число было преобразовано в беззнаковое).

С стандартизирован по более сложным правилам "сохранения стоимости". В соответствии с правилами сохранения значений повышение может зависеть от фактических диапазонов типов, поэтому вы можете получить разные результаты на разных компиляторах. Например, на большинстве компиляторов MS-DOS, int такой же размер как short а также long отличается от любого. На многих современных системах int такой же размер как long, а также short отличается от любого. С правилами сохранения значений это может привести к тому, что продвигаемый тип будет отличаться между ними.

Основная идея правил сохранения значений заключается в том, что он будет преобразован в больший тип со знаком, если он может представлять все значения меньшего типа. Например, 16-битный unsigned short может быть повышен до 32-разрядного signed intпотому что каждое возможное значение unsigned short может быть представлен в виде signed int, Типы будут переведены в тип без знака тогда и только тогда, когда это необходимо для сохранения значений меньшего типа (например, если unsigned short а также signed int оба 16 бит, то signed int не может представлять все возможные значения unsigned shortтак unsigned short будет повышен до unsigned int).

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

Если вы не назначите результат, такой как в сравнении, все может стать довольно уродливым. Например:

unsigned int a = 5;
signed int b = -5;

if (a > b)
    printf("Of course");
else
    printf("What!");

Под знаком сохранения правил, b будет повышен до неподписанного, и в процессе станет равным UINT_MAX - 4так что "Что!" нога if будет принято. С помощью правил сохранения значений вы также можете получить некоторые странные результаты, немного похожие на это, но 1) в основном на DOS-подобных системах, где int такой же размер как shortи 2) как правило, в любом случае это сделать сложнее.

Он выбирает любой тип, в который вы помещаете свой результат, или, по крайней мере, cout учитывает этот тип во время вывода.

Я не помню точно, но я думаю, что компиляторы C++ генерируют один и тот же арифметический код для обоих, это только сравнение и вывод, которые заботятся о знаке.

Другие вопросы по тегам