8.4 – Преобразования при вычислении арифметических выражений

Добавлено 20 июня 2021 в 02:53

В уроке «5.1 – Приоритет и ассоциативность операторов» мы обсудили, как выражения вычисляются в соответствии с приоритетом и ассоциативностью их операторов.

Рассмотрим следующее выражение:

auto x { 2 + 3 };

Когда вызывается бинарный operator+, ему передаются два операнда, оба типа int. Поскольку оба операнда относятся к одному типу, этот тип будет использоваться для выполнения вычисления и возврата результата. Таким образом, 2 + 3 будет вычисляться как int значение 5.

Но что происходит, когда операнды бинарного оператора имеют разные типы?

auto y { 2 + 3.5 };

В этом случае оператору + присваивается один операнд типа int, а другой – типа double. Результат оператора должен возвращаться как int, double или, возможно, что-то еще?

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

Операторы, которым требуются операнды одного типа

Следующие операторы требуют, чтобы их операнды были одного типа:

  • бинарные арифметические операторы: +, -, *, /
  • бинарные операторы отношения: <, >, <=, >=, ==, !=
  • бинарные побитовые арифметические операторы: &, ^, |
  • условный оператор ?: (исключая условие, которое, как ожидается, будет иметь тип bool)

Правила обычного арифметического преобразования

Правила обычного арифметического преобразования довольно просты. У компилятора есть список типов по приоритету, который выглядит примерно так:

  1. long double (высший приоритет)
  2. double
  3. float
  4. unsigned long long
  5. long long
  6. unsigned long
  7. long
  8. unsigned int
  9. 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.

Это одна из основных причин избегать целочисленных типов без знака – когда в арифметических выражениях вы смешиваете их с целочисленными типами со знаком, вы рискуете получить неожиданные результаты. А компилятор, вероятно, даже не выдаст предупреждения.

Теги

C++ / CppLearnCppДля начинающихНеявное преобразование типаОбучениеПриведение типовПрограммированиеРасширяющее преобразование типаСужающее преобразование типаЧисловое продвижение

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.