8.2 – Продвижение целочисленных типов и типов с плавающей запятой
В уроке «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()
литерал float
4.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 – Разрешение перегрузки функций и неоднозначные совпадения»).