6.15 – Неявное преобразование (принуждение) типов данных

Добавлено 22 мая 2021 в 11:54

Ранее вы узнали, что значение переменной хранится как последовательность битов, а тип данных переменной сообщает компилятору, как интерпретировать эти биты в осмысленные значения. Различные типы данных могут по-разному представлять «одно и то же» число: например, целочисленное значение 3 и значение с плавающей запятой 3.0 хранятся как совершенно разные двоичные шаблоны.

Так что же происходит, когда мы делаем что-то подобное?

float f{ 3 }; // инициализируем переменную с плавающей запятой с помощью значения int, 3

В таком случае компилятор не может просто скопировать биты, представляющие значение int 3, в переменную f типа float. Вместо этого ему необходимо преобразовать целочисленное значение 3 в число с плавающей запятой, которое затем может быть присвоено переменной f типа float.

Процесс преобразования значения из одного типа данных в другой называется преобразованием типа. Преобразование типа может происходить во многих разных случаях:

  • при присвоении или инициализации переменной значением другого типа данных:
    double d{ 3 }; // инициализируем переменную double целочисленным значением 3
    d = 6;         // присваиваем переменной типа double целочисленное значение 6
  • при передаче значения функции, где параметр функции имеет другой тип данных:
    void doSomething(long l)
    {
    }
     
    doSomething(3); // передаем значение int 3 функции, ожидающей параметр long
  • при возврате значения из функции, в которой тип возвращаемого значения функции имеет другой тип данных:
    float doSomething()
    {
        return 3.0; // Возвращаем значение double 3.0 обратно вызывающему через 
                    // тип возвращаемого значения float
    }
  • использование бинарного оператора с операндами разных типов:
    double division{ 4.0 / 3 }; // деление с double и int

Во всех этих случаях (и в некоторых других) C++ будет использовать преобразование типа для преобразования одного типа данных в другой тип данных.

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

В этом уроке мы рассмотрим неявное преобразование типа, а в следующем – явное преобразование типа.

Неявное преобразование типа

Неявное преобразование типа (также называемое автоматическим преобразованием типа или принуждением, англоязычный термин – «coecion») выполняется всякий раз, когда ожидается один тип данных, но предоставляется другой тип данных. Если компилятор может выяснить, как выполнить преобразование между этими двумя типами, он это сделает. Если он не знает, как это сделать, он выдаст ошибку компиляции.

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

Существует два основных типа неявного преобразования типов: продвижение и преобразование.

Числовое продвижение

Всякий раз, когда значение из одного базового типа данных преобразуется в значение большего базового типа данных из того же семейства, это называется числовым продвижением (или расширяющим преобразованием, хотя этот термин обычно зарезервирован для целочисленных типов). Например, int можно расширить до long, а float – до double:

long l{ 64 };      // расширяем int 64 до long
double d{ 0.12f }; // преобразуем float 0.12 в double

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

  • целочисленное продвижение включает в себя преобразование целочисленных типов, более узких, чем int (включая bool, char, unsigned char, signed char, unsigned short и signed short) в int (если возможно) или unsigned int (в противном случае);
  • продвижение типов плавающей запятой включает в себя преобразование float в double.

Целочисленное продвижение и продвижение типов с плавающей запятой используются в определенных случаях для преобразования меньших типов данных в int/unsigned int или double, потому что int и double обычно являются наиболее производительными типами для выполнения операций.

О продвижениях важно помнить, что они всегда безопасны и не приводят к потере данных.

Для продвинутых читателей


Под капотом, продвижение обычно включает в себя расширение двоичного представления числа (например, для целочисленных типов – добавление ведущих нулей).

Числовые преобразования

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

// преобразуем int 3 в double (между разными типами)
double d{ 3 };

// преобразуем int  2 в short (от большего типа к меньшему
// в пределах того же семейства типов)
short s{ 2 };

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

Правила преобразования сложны и многочисленны, поэтому здесь мы рассмотрим только общие случаи.

Во всех случаях преобразование значения в тип с недостаточно большим диапазоном для поддержки заданного значения приведет к неожиданным результатам. Например:

int main()
{
    int i{ 30000 };
    char c = i; // char имеет диапазон от -128 до 127
 
    std::cout << static_cast<int>(c);
 
    return 0;
}

В этом примере мы присвоили char (диапазон значений от -128 до 127) большое целое число. Это вызывает переполнение char и дает неожиданный результат:

48

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

int i{ 2 };
short s = i; // конвертируем из int в short
std::cout << s << '\n';
 
double d{ 0.1234 };
float f = d;
std::cout << f << '\n';

Этот код дает ожидаемый результат:

2
0.1234

В случае значений с плавающей запятой может произойти некоторое округление из-за потери точности в меньшем типе. Например:

// значение double 0.123456789 имеет 9 значащих цифр,
// но float может поддерживать только около 7
float f = 0.123456789; 

// std::setprecision определен в заголовке iomanip
std::cout << std::setprecision(9) << f << '\n';

В этом случае мы видим потерю точности, потому что float не может поддерживать такую же точность, как double:

0.123456791

Преобразование целочисленного типа в тип с плавающей запятой обычно работает до тех пор, пока значение попадает в диапазон типа с плавающей запятой. Например:

int i{ 10 };
float f = i;
std::cout << f;

Это дает ожидаемый результат:

10

Преобразование из типа с плавающей запятой в целочисленный тип работает до тех пор, пока значение попадает в диапазон целочисленного типа, но любые дробные части теряются. Например:

int i = 3.5;
std::cout << i << '\n';

В этом примере дробная часть (.5) теряется, в результате остается следующий результат:

3

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

double d{ 10.0 };
int i{ d }; // Ошибка: double может хранить значения, которые не помещаются в int

Вычисление арифметических выражений

При вычислении выражений компилятор разбивает каждое выражение на отдельные подвыражения. Арифметические операторы требуют, чтобы их операнды были одного типа. Для этого компилятор использует следующие правила:

  • если операнд принадлежит целочисленому типу, который уже, чем int, он подвергается целочисленному расширяющему преобразованию (как описано выше) в int или unsigned int;
  • если операнды по-прежнему не совпадают, компилятор находит операнд с наивысшим приоритетом и неявно преобразует другой операнд в соответствующий тип.

Приоритет операндов следующий:

  1. long double (высший приоритет)
  2. double
  3. float
  4. unsigned long long
  5. long long
  6. unsigned long
  7. long
  8. unsigned int
  9. int (низший приоритет)

Мы можем увидеть, как обычно происходит числовое преобразование, с помощью оператора typeid (включенного в заголовок <typeinfo>), который можно использовать для отображения результирующего типа выражения.

В следующем примере мы складываем два значения 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;
}

Поскольку short является целочисленным типом, перед складыванием над операндами выполняется целочисленное продвижение до int. Результатом суммы двух int является int, как и следовало ожидать:

int 9

Примечание. Ваш компилятор может отображать это немного по-другому, поскольку формат typeid.name() оставлен на усмотрение компилятора.

Давайте рассмотрим другой случай:

#include <iostream>
#include <typeinfo> // для typeid()
 
int main()
{
    double d{ 4.0 };
    short s{ 2 };
    std::cout << typeid(d + s).name() << ' ' << d + s << '\n'; // покажем нам тип d + s
 
    return 0;
}

В этом случае над short выполняется целочисленное продвижение до int. Однако int и double по-прежнему не совпадают. Поскольку double находится выше в иерархии типов, int 2 преобразуется в double 2.0, и числа double складываются для получения результата double.

double 6.0

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

std::cout << 5u - 10; // 5u означает рассматривать 5 как unsigned int

можно ожидать, что выражение 5u - 10 будет вычисляться как -5, поскольку 5 - 10 = -5. Но вот что происходит на самом деле:

4294967291

В этом случае signed int 10 продвигается до unsigned int (который имеет более высокий приоритет), и выражение вычисляется как unsigned int. Поскольку -5 не может быть сохранено в unsigned int, вычисление выполняет перенос, и мы получаем ответ, которого не ожидаем.

Это одна из многих веских причин избегать целочисленных типов без знака в целом.

Небольшой тест

Вопрос 1

В чем разница между числовым продвижением и числовым преобразованием?

Числовое продвижение – это преобразование меньшего типа (обычно целочисленного или с плавающей запятой) в более крупный аналогичный (целочисленный или с плавающей запятой) тип. Продвижение обычно включает в себя расширение двоичного представления числа (например, для целых чисел добавление ведущих нулей).

Числовое преобразование – это преобразование большего типа в меньший или между разными типами. Преобразования требуют преобразования базового двоичного представления в другой формат.

Теги

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

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

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