8.2 – Продвижение целочисленных типов и типов с плавающей запятой

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

В уроке «4.3 – Размеры объектов и оператор sizeof» мы отметили, что в C++ есть гарантии минимального размера для каждого из базовых типов. Однако фактический размер этих типов может варьироваться в зависимости от компилятора и архитектуры.

Эта вариативность была разрешена, поэтому для типов данных int и double можно было задать размер, который максимизирует производительность в данной архитектуре. Например, 32-битный компьютер обычно может обрабатывать 32-битные данные за раз. В таких случаях int, вероятно, будет иметь ширину 32 бита, поскольку это «естественный» размер данных, с которыми работает CPU (и, вероятно, будет наиболее производительным).

Напоминание


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

Но что происходит, когда мы хотим, чтобы наш 32-битный процессор изменял 8-битное значение (например, символ) или 16-битное значение? Некоторые 32-битные процессоры (например, серия x86) могут напрямую манипулировать 8-битными и 16-битными значениями. Однако часто это происходит медленнее, чем манипулирование 32-битными значениями! Другие 32-разрядные процессоры (например, PowerPC) могут работать только с 32-разрядными значениями, и для манипулирования более узкими значениями необходимо использовать дополнительные приемы.

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

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

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

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

Числовое продвижение снижает избыточность

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

#include <iostream>
 
void printInt(int x)
{
    std::cout << x;
}

Хотя тут всё просто, но что произойдет, если мы захотим также иметь возможность печатать значение типа short или типа char? Если бы преобразования типов не существовало, нам пришлось бы написать еще одну функцию print для short и одну для char. И не забудьте еще версию для unsigned char, signed char, unsigned short, wchar_t, char8_t, char16_t и char32_t! Вы можете видеть, как это быстро становится неуправляемым.

Здесь на помощь приходит числовое продвижение: мы можем писать функции с параметрами int и/или double (например, функция printInt() выше). Затем этот же код можно вызвать с аргументами типов, которые можно численно продвигать для совпадения с типами параметров функции.

Категории числового продвижения

Числовые правила продвижения делятся на две подкатегории: целочисленное продвижение и продвижение типов с плавающей запятой.

Продвижение типов с плавающей запятой

Начнем с более простого.

Используя правила продвижения типов с плавающей запятой, значение типа float может быть преобразовано в значение типа double.

Это означает, что мы можем написать функцию, которая принимает значение типа double, а затем вызывать ее со значением типа double или float:

#include <iostream>
 
void printDouble(double d)
{
    std::cout << d;
}
 
int main()
{
    printDouble(5.0);  // преобразование не требуется
    printDouble(4.0f); // числовое продвижение float в double
 
    return 0;
}

Во втором вызове printDouble() литерал float4.0f продвигается до double, поэтому тип аргумента соответствует типу параметра функции.

Целочисленные продвижения

Правила целочисленного продвижения более сложны.

Используя правила целочисленного продвижения, можно сделать следующие преобразования:

  • signed char или signed short можно преобразовать в int
  • unsigned char, char8_t и unsigned short могут быть преобразованы в int, если int может содержать весь диапазон типа, или в unsigned int в противном случае.
  • char может быть преобразован в int (по умолчанию, если char со знаком) или unsigned int (по умолчанию, если char без знака)
  • bool можно преобразовать в int, при этом false становится 0, а true становится 1

Есть еще несколько других неотъемлемых правил продвижения, которые используются реже. Их можно найти по адресу
https://en.cppreference.com/w/cpp/language/implicit_conversion#Integral_promotion.

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

#include <iostream>
 
void printInt(int x)
{
    std::cout << x;
}
 
int main()
{
    printInt(2);
 
    // нет суффикса литерала short, поэтому для этого мы будем использовать переменную
    short s{ 3 };
    // числовое продвижение short в int 
    printInt(s); 
 
    printInt('a');  // числовое продвижение char в int
    printInt(true); // числовое продвижение bool в int
 
    return 0;
}

Здесь стоит отметить две вещи. Во-первых, в некоторых системах некоторые из целочисленных типов могут быть преобразованы в unsigned int, а не в int. Во-вторых, некоторые более узкие беззнаковые типы (такие как unsigned char) будут преобразованы в более крупные типы со знаком (например, int). Таким образом, хотя целочисленное продвижение способствует сохранению значения, оно не обязательно сохраняет отсутствие знака.

Не все преобразования, сохраняющие значения, являются числовыми продвижениями

Некоторые преобразования типов с сохранением значений (например, int в long или int в double) в C++ не считаются числовыми продвижениями (это числовые преобразования, которые мы вскоре рассмотрим в уроке «8.3 – Числовые преобразования»).

Различие носит в основном академический характер. Однако в некоторых случаях компилятор предпочитает числовые продвижения числовым преобразованиям. Мы увидим примеры, в которых это имеет значение, когда мы рассмотрим разрешение перегрузки функций (в следующем уроке «8.11 – Разрешение перегрузки функций и неоднозначные совпадения»).

Теги

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

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

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