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(высший приоритет)doublefloatunsigned long longlong longunsigned longlongunsigned intint(низший приоритет)
Здесь есть только два правила:
- если тип любого из операндов находится в списке приоритетов, операнд с более низким приоритетом преобразуется в тип операнда с более высоким приоритетом;
- в противном случае (ни один из типов операндов не указан в списке) над обоими операндами выполняется числовое продвижение (смотрите «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.
Это одна из основных причин избегать целочисленных типов без знака – когда в арифметических выражениях вы смешиваете их с целочисленными типами со знаком, вы рискуете получить неожиданные результаты. А компилятор, вероятно, даже не выдаст предупреждения.
