8.4 – Преобразования при вычислении арифметических выражений
В уроке «5.1 – Приоритет и ассоциативность операторов» мы обсудили, как выражения вычисляются в соответствии с приоритетом и ассоциативностью их операторов.
Рассмотрим следующее выражение:
auto x { 2 + 3 };
Когда вызывается бинарный operator+
, ему передаются два операнда, оба типа int
. Поскольку оба операнда относятся к одному типу, этот тип будет использоваться для выполнения вычисления и возврата результата. Таким образом, 2 + 3 будет вычисляться как int
значение 5.
Но что происходит, когда операнды бинарного оператора имеют разные типы?
auto y { 2 + 3.5 };
В этом случае оператору +
присваивается один операнд типа int
, а другой – типа double
. Результат оператора должен возвращаться как int
, double
или, возможно, что-то еще?
В C++ некоторые операторы требуют, чтобы их операнды были одного типа. Если один из этих операторов вызывается с операндами разных типов, один или оба операнда будут неявно преобразованы в соответствующие типы с использованием набора правил, называемых обычными арифметическими преобразованиями.
Операторы, которым требуются операнды одного типа
Следующие операторы требуют, чтобы их операнды были одного типа:
- бинарные арифметические операторы:
+
,-
,*
,/
- бинарные операторы отношения:
<
,>
,<=
,>=
,==
,!=
- бинарные побитовые арифметические операторы:
&
,^
,|
- условный оператор
?:
(исключая условие, которое, как ожидается, будет иметь типbool
)
Правила обычного арифметического преобразования
Правила обычного арифметического преобразования довольно просты. У компилятора есть список типов по приоритету, который выглядит примерно так:
long double
(высший приоритет)double
float
unsigned long long
long long
unsigned long
long
unsigned int
int
(низший приоритет)
Здесь есть только два правила:
- если тип любого из операндов находится в списке приоритетов, операнд с более низким приоритетом преобразуется в тип операнда с более высоким приоритетом;
- в противном случае (ни один из типов операндов не указан в списке) над обоими операндами выполняется числовое продвижение (смотрите «8.2 – Продвижение целочисленных типов и типов с плавающей запятой»).
Несколько примеров
В следующих примерах мы будем использовать оператор typeid
(включен в заголовок <typeinfo>
), чтобы показать результирующий тип выражения.
Сначала добавим int
и double
:
#include <iostream>
#include <typeinfo> // для typeid()
int main()
{
int i{ 2 };
double d{ 3.5 };
std::cout << typeid(i + d).name() << ' ' << i + d << '\n'; // покажет тип i + d
return 0;
}
В этом случае операнд double
имеет более высокий приоритет, поэтому операнд с более низким приоритетом (типа int
) преобразуется в тип double
со значением 2.0. Затем значения double
2.0 и 3.5 складываются, чтобы получить результат double
5.5.
На машине автора этот код печатает:
double 5.5
Обратите внимание, что ваш компилятор может отображать что-то немного другое, так как вывод typeid.name()
оставлен на усмотрение компилятора.
Теперь сложим два значения типа short
:
#include <iostream>
#include <typeinfo> // для typeid()
int main()
{
short a{ 4 };
short b{ 5 };
std::cout << typeid(a + b).name() << ' ' << a + b << '\n'; // покажет тип a + b
return 0;
}
Поскольку ни один из операндов не появляется в списке приоритетов, оба операнда проходят целочисленное продвижение до типа int
. Результатом сложения двух чисел int
является int
, как и следовало ожидать:
int 9
Проблемы со знаком / без знака
Эта иерархия приоритетов может вызвать некоторые проблемы при смешивании значений со знаком и без знака. Например, взгляните на следующий код:
#include <iostream>
#include <typeinfo> // для typeid()
int main()
{
// 5u означает рассматривать 5 как unsigned int
std::cout << typeid(5u-10).name() << ' ' << 5u - 10;
return 0;
}
Вы можете ожидать, что выражение 5u - 10 будет вычисляться как -5, поскольку 5-10 = -5. Но вот что на самом деле получается:
unsigned int 4294967291
Поскольку операнд unsigned int
имеет более высокий приоритет, операнд int
преобразуется в unsigned int
. А поскольку значение -5 выходит за пределы диапазона unsigned int
, мы получаем результат, которого не ожидаем.
Вот еще один пример, показывающий противоречивый результат:
#include <iostream>
int main()
{
std::cout << std::boolalpha << (-3 < 5u);
return 0;
}
Хотя нам ясно, что 5 больше, чем -3, при вычислении этого выражения -3 преобразуется в большое число unsigned int
, которое больше 5. Таким образом, приведенный выше код выводит false
, а не ожидаемый результат, равный true
.
Это одна из основных причин избегать целочисленных типов без знака – когда в арифметических выражениях вы смешиваете их с целочисленными типами со знаком, вы рискуете получить неожиданные результаты. А компилятор, вероятно, даже не выдаст предупреждения.