# Глава 14.1. Фундаментальные типы
Тип есть у любого значения, выражения или функции. Если он не задан явно, то выводится компилятором.
```cpp {.example_for_playground .example_for_playground_001}
// Тип результата деления - double
std::println("{}", 34.2 / 8);
```
```
4.275
```
Так, в этом примере тип литерала 34.2 — `double`, а тип литерала 8 — `int`. И по [правилам математических преобразований,](https://timsong-cpp.github.io/cppwp/n4950/expr.arith.conv) если в математической операции участвует тип с плавающей точкой, то и результат — тоже тип с плавающей точкой.
Тип определяет:
- Сколько памяти выделяется под переменную или результат выражения.
- Как компилятор трактует последовательность бит в этой памяти.
- Какие значения могут в ней храниться.
- Какие над ними допустимы операции.
В C++ типы данных [делятся](https://timsong-cpp.github.io/cppwp/n4868/basic.types) на две категории:
- [Фундаментальные типы](https://timsong-cpp.github.io/cppwp/n4868/basic.fundamental) (fundamental types): `int`, `bool` и другие. Это кирпичики, из которых строятся составные типы.
- [Составные типы](https://timsong-cpp.github.io/cppwp/n4868/basic.compound) (compound types) основаны на других типах. Яркий пример — структуры.
 {.illustration}
**Фундаментальные типы** также называют встроенными (built-in), ведь они считаются неотъемлемой частью языка. Их можно назвать удобной оберткой над представлением данных в архитектуре компьютера. Операции с фундаментальными типами как правило сводятся к единственной инструкции на ассемблере и выполняются максимально эффективно.
Размер этих типов фиксирован и зависит от реализации компилятора, ОС и целевой платформы. Выяснить размер можно с помощью оператора [sizeof](https://en.cppreference.com/w/cpp/language/sizeof.html), который принимает тип и возвращает выделяемое под него количество байт:
```cpp {.example_for_playground .example_for_playground_002}
const std::size_t n = sizeof(int);
std::println("{}", n);
```
```
4
```
К фундаментальным типам относятся:
- [Арифметические типы.](https://timsong-cpp.github.io/cppwp/n4868/basic.fundamental#12) Например, `int` и `char`.
- Тип с пустым набором значений `void`. Вы уже использовали его как тип возвращаемого значения функции, которая ничего не возвращает. У `void` есть и другие применения, о которых вы скоро узнаете.
- Тип для обозначения нулевого указателя `std::nullptr_t`. Про указатели вы узнаете чуть позже.
Имена фундаментальных типов являются ключевыми словами (keywords) языка. Для их использования не надо подключать никаких хедеров или модулей. Исключение — добавленный в C++11 `std::nullptr_t`.
 {.illustration}
С `void` и несколькими арифметическими типами вы уже [знакомы.](/courses/cpp/chapters/cpp_chapter_0020/#block-fundamental-types) Пора узнать про остальные.
**Арифметические типы** описывают:
- Целые числа ([integral types](https://timsong-cpp.github.io/cppwp/n4868/basic.fundamental#11) или integer types).
- Числа с плавающей точкой (floating-point types).
 {.illustration}
## Целые числа на базе int
Основной целочисленный тип для работы со знаковыми целыми — это `int`. Стандарт гарантирует, что он имеет ширину как минимум 16 бит. То есть под переменные типа `int` выделяется _не меньше_ 16 бит памяти. В 32/64-битных архитектурах ширина `int` почти всегда начинается от 32-х бит.
На базе `int` легко получить другой целочисленный тип. Для этого к типу добавляются _модификаторы_ — ключевые слова, задающие:
- Минимальную ширину.
- `short` — 16 бит и больше.
- `long` — 32 бита и больше.
- `long long` — 64 бита и больше.
- Является ли тип знаковым.
- `signed` — знаковый тип.
- `unsigned` — беззнаковый тип.
```cpp {.example_for_playground .example_for_playground_003}
// Беззнаковое целое шириной минимум 32 бита
unsigned long int max_recursive_calls;
```
[Диапазон знакового целого числа](https://timsong-cpp.github.io/cppwp/n4868/basic.fundamental) — от `−2^(N - 1)` до `2^(N - 1) − 1` включительно, где `N` — ширина типа в битах. {.task_text}
Напишите через пробел два целых числа, описывающих диапазон значений типа `short` в нашем плэйграунде. Вы можете узнать его, открыв [плэйграунд](https://senjun.ru/playground/cpp/) и применив оператор `sizeof`. {.task_text}
```consoleoutput {.task_source #cpp_chapter_0141_task_0010}
```
Вам нужно узнать, сколько бит занимает тип `short`. Затем подставьте это знаение в формулу `2^(N - 1)`. Здесь `^` означает возведение в степень. Получившееся значение с отрицательным знаком — нижняя граница диапазона. А с положительным знаком и на единицу меньше — верхняя граница. Напишите их через пробел. {.task_hint}
```cpp {.task_answer}
-32768 32767
```
Модификаторы из разных категорий можно комбинировать, а из одной и той же — нельзя:
```cpp {.example_for_playground .example_for_playground_004}
signed long int mantissa_with_sign; // ок
long short int strange; // ошибка компиляции
```
Модификаторы и ключевое слово `int` могут идти в любом порядке:
```cpp {.example_for_playground .example_for_playground_005}
// У sysno и interval один и тот же тип
signed long int sysno;
long int signed interval;
```
Но все же модификаторы принято ставить вначале:
```cpp
long int interval;
```
По умолчанию целочисленный тип знаковый, даже без модификатора `signed`:
```cpp
short int min_int_in_pascal = -32768;
```
Если указан любой из модификаторов, `int` можно опустить:
```cpp
// Эквивалентно типу unsigned long long int
unsigned long long threshold_bytes;
```
Кстати, для удобства чтения разряды целого числа можно разделять:
```cpp
unsigned mln = 1'000'000;
```
Номер [порта](https://ru.wikipedia.org/wiki/%D0%9F%D0%BE%D1%80%D1%82_(%D0%BA%D0%BE%D0%BC%D0%BF%D1%8C%D1%8E%D1%82%D0%B5%D1%80%D0%BD%D1%8B%D0%B5_%D1%81%D0%B5%D1%82%D0%B8)) — это число в диапазоне от `1` до `2^16 - 1` (65 535). {.task_text}
Какой тип подойдет для его хранения больше всего? {.task_text}
```consoleoutput {.task_source #cpp_chapter_0141_task_0020}
```
Диапазон беззнакового целого — от `0` до `2^N − 1` включительно, где `N` — ширина типа в битах. Диапазон номеров портов — от `1` до `2^16 - 1`. Для хранения числа из этого диапазона будет достаточно 16 бит. Столько занимает `short`. Так как порт не может быть отрицательным, то лучше всего подходит тип `unsigned short`. {.task_hint}
```cpp {.task_answer}
unsigned short
```
### Целочисленное переполнение
Целочисленное переполнение (integer overflow) — это ситуация, при которой значение не помещается в тип данных. Обработка переполнения для знаковых и беззнаковых чисел отличается.
**Переполнение беззнакового типа** приводит к отбрасыванию старших битов числа. Это гарантирует цикличность значений, как если бы мы применяли к числу деление по модулю `2^N`, где `N` — ширина типа в битах.
Сохраним в переменную `x` максимальное для ее типа значение. Его можно получить вызовом шаблонной функции [std::numeric_limits<T>::max()](https://en.cppreference.com/w/cpp/types/numeric_limits/max.html). Инкрементируем это значение, вызвав тем самым переполнение:
```cpp {.example_for_playground .example_for_playground_006}
unsigned x = std::numeric_limits<unsigned>::max();
std::println("{}", x);
++x; // переполнение сверху
std::println("{}", x);
```
```
4294967295
0
```
Из-за цикличности значений беззнаковых типов за максимальным значением последовало минимальное, то есть 0.
А теперь переполним число снизу, а не сверху:
```cpp {.example_for_playground .example_for_playground_007}
unsigned x = -1; // переполнение снизу
std::println("{}", x);
std::println("{}", x == std::numeric_limits<unsigned>::max());
```
```
4294967295
true
```
Мы вновь убедились в цикличности значений беззнаковых типов.
**Переполнение знакового типа** — это UB. Большинство реализаций комплияторов обрабатывают его как переполнение беззнакового целого, но полагаться на это нельзя. UB нужно избегать. Типичные причины возникновения переполнения:
- Неправильно подобранные типы.
- Суммирование или перемножение чисел с сохранением результата в переменную недостаточной ширины.
- Неявное приведение типов.
Реализуйте функцию `checked_sum()`, которая производит сложение двух целых чисел без переполнения. Если сложение может привести к переполнению, то функция возвращает: {.task_text}
- `std::numeric_limits<int>::max()` для положительных слагаемых.
- `std::numeric_limits<int>::min()` для отрицательных слагаемых.
```cpp {.task_source #cpp_chapter_0141_task_0030}
int checked_sum(int a, int b)
{
}
```
Для положительных чисел переполнение возникает, если выполняется условие`a > std::numeric_limits<int>::max() - b`. Для отрицательных переполнение возникает, если выполняется условие`a < std::numeric_limits<int>::min() - b`. Достаточно проверять на знак только число `b`. {.task_hint}
```cpp {.task_answer}
int checked_sum(int a, int b)
{
if (b > 0 && a > std::numeric_limits<int>::max() - b)
return std::numeric_limits<int>::max();
if (b < 0 && a < std::numeric_limits<int>::min() - b)
return std::numeric_limits<int>::min();
return a + b;
}
```
В реализации компилятора у нас на площадке переполнение знакового типа обрабатывается так же, как и беззнакового:
```cpp {.example_for_playground .example_for_playground_008}
short val = 1'000'000; // сужающее преобразование
std::println("{}", val);
std::size_t len_bits = sizeof(short) * 8;
std::size_t mod = std::pow(2, len_bits); // возведение в степень
std::println("{}", val == 1'000'000 % mod);
```
```
16960
true
```
Перед вами функция `naive_hash()` из [Путеводителя C++ программиста по неопределенному поведению.](https://github.com/Nekrolm/ubbook/blob/master/numeric/overflow.md) Она неправильно считает хеш от строки. В ней допущено UB, а конкретно — переполнение знакового целого. Переполнение происходит моментально, например при вызове `naive_hash("*")`. В переменную `h` типа `short` записывается слишком большое значение. Откройте задачу в плэйграунде и запустите проект. Так вы убедитесь в некорректном поведении функции. {.task_text}
Опробуйте способ для _выявления_ переполнения. Замените приводящее к переполнению выражение `h += h * 15'983 + c` на [универсальную инициализацию:](/courses/cpp/chapters/cpp_chapter_0131/#block-uniform-initialization) `h = {h + h * 15'983 + c}`. Это приведет к запрету сужающего преобразования. Компилятор выдаст ошибку, подсказывающую, что в коде допущено переполнение! {.task_text}
А теперь избавьтесь от переполнения. Замените возвращаемый функцией тип и соответственно тип переменной на более подходящий для хеша длинной строки и исключающий UB при переполнении. {.task_text}
```cpp {.task_source #cpp_chapter_0141_task_0040}
short naive_hash(std::string s)
{
short h = 15;
for (char c : s)
{
h += h * 15'983 + c;
}
return h;
}
```
Возьмите беззнаковый тип максимальной ширины. {.task_hint}
```cpp {.task_answer}
unsigned long long naive_hash(std::string s)
{
unsigned long long h = 15;
for (char c : s)
{
h += h * 15'983 + c;
}
return h;
}
```
### Целые числа в разных системах счисления
По умолчанию все числа имеют десятичное основание. Для литералов с другим основанием нужно использовать префикс. Он предназначен _только для целых чисел_ и подсказывает компилятору, какая выбрана система счисления.
- `0b` или `0B` — двоичная: `0b1101`, `0B01`.
- `0` — восьмеричная: `073`, `0513`.
- `0x` или `0X` — шестнадцатеричная: `0x01f`, `0XF3AAB`.
```cpp {.example_for_playground .example_for_playground_009}
int oct = 011;
std::println("{}", oct);
```
```
9
```
Обратите внимание: начинающиеся с нуля литералы компилятор трактует как числа в восьмеричной системе:
```cpp {.example_for_playground .example_for_playground_010}
int oct = 09;
std::println("{}", oct);
```
```
error: invalid digit '9' in octal constant
```
При форматировании строк или печати в консоль числа выводятся в десятичной системе счисления. Чтобы это переопределить, используются [спецификаторы форматирования.](https://en.cppreference.com/w/cpp/utility/format/spec.html) Это символы, идущие в строке форматирования внутри фигурных скобок после двоеточия:
```cpp {.example_for_playground .example_for_playground_011}
unsigned rgb_pink = 0xf004ed;
// Спецификатор x указывает, что число нужно
// вывести в 16-ой системе счисления
std::println("{:x}", rgb_pink);
```
```
f004ed
```
Через спецификаторы форматирования можно тонко настраивать вид строки, в том числе отступы, выравнивание и многое другое.
## Символьные типы
Как и следует из названия, основное предназначение символьных типов — хранить код символа. Он может быть представлен в виде целого числа:
```cpp {.example_for_playground .example_for_playground_012}
char symbol = 'Q';
char ascii_code = 81;
std::println("{}", symbol == ascii_code);
```
```
true
```
Всего стандарт описывает семь символьных типов: `signed char`, `unsigned char`, `char`, `wchar_t`, `char8_t`, `char16_t` и `char32_t`. Рассмотрим, какая между ними разница.
Типы `signed char` и `unsigned char` нужны для знакового и беззнакового представления числа. Например, для хранения ASCII-кодов. Тип `unsigned char` также популярен для работы с данными в бинарном виде:
```cpp
std::vector<unsigned char> raw_bytes;
```
Тип `char` может иметь или не иметь знак в зависимости от целевой платформы, компилятора и его [настроек.](https://clang.llvm.org/docs/ClangCommandLineReference.html#cmdoption-clang-fsigned-char) Поэтому `char` довольно опасен как тип для работы с числами. **Никогда** не полагайтесь на наличие и отсутствие знака у `char`. При сборке под архитектуры x86-64 `char` как правило знаковый, а под ARM — беззнаковый.
На что вы можете полагаться — так это на то, что размер `char` _всегда_ равен 1 байту. Как и размер знакового и беззнакового `char`:
```cpp
sizeof(char) == sizeof(unsigned char) == sizeof(signed char) == 1
```
Значит, диапазон знакового `char` — от -128 до 127, а беззнакового — от 0 до 255. Конечно, при условии, что в байте 8 бит. А стандарт этого **не обещает.** Чисто гипотетически байт может быть [больше.](https://isocpp.org/wiki/faq/intrinsic-types#bits-per-byte) Например, состоять из 9, 36 или 64 бит. Но помнить об этом стоит только если ваш код запускается на экзотических и архаичных архитектурах.
Знаковые и беззнаковые символы можно преобразовывать туда-обратно с уверенностью, что исходное значение не исказится:
```cpp {.example_for_playground .example_for_playground_013}
unsigned char a = 251;
signed char b = static_cast<signed char>(a);
unsigned char c = static_cast<unsigned char>(b);
std::println("{}", a == c);
```
```
true
```
Эта гарантия работает также для преобразований `char` <-> `signed char` и `char` <-> `unsigned char`. Но откуда она взялась, если у знаковых и беззнаковых символов разные диапазоны значений? Все дело в том, что у этих типов совпадает размер и битовое представление в памяти. Просто в зависимости от типа переменной компилятор по-разному интерпретирует записанную в нее последовательность бит:
```cpp {.example_for_playground .example_for_playground_023}
unsigned char a = 251;
signed char b = static_cast<signed char>(a);
std::println("unsigned char. Bits: {}. Value: {}",
bits_str(a), a);
std::println(" signed char. Bits: {}. Value: {}",
bits_str(bs), b);
```
```
unsigned char. Bits: 11111011. Value: 251
signed char. Bits: 11111011. Value: -5
```
Взгляните на класс `CharTable` из [Путеводителя C++ программиста по неопределенному поведению.](https://github.com/Nekrolm/ubbook/blob/master/numeric/char_sign_extension.md) Что будет, если вызвать метод `is_printable()` от значения типа `char`, равного 128? {.task_text}
Если вы внимательно прочли этот раздел главы, то уже поняли, что в коде есть проблемы. Откройте задачу в плэйграунде, в `CMakeLists.txt` уберите флаг компилятора `-Werror` и запустите проект. Вместо ожидаемого вывода `false` вы увидите произвольную строку. {.task_text}
Первая проблема этого кода в том, что он полагается на беззнаковость `char`. Но в плэйграунде `char` знаковый, и значение 128 приводит к переполнению знакового целого, то есть к UB. Компилятор в плэйграунде обрабатывает переполнение, сохраняя в переменную типа `char` отрицательное значение. С другим компилятором мы могли бы получить совершенно другое поведение. {.task_text}
Вторая проблема — это неявное преобразование знакового `char` к беззнаковому `std::size_t` в момент взятия элемента массива по индексу: `m_is_printable[c]`. Оператор `[]` работает с индексом типа `std::size_t`, к которому и приводится `char`. Из отрицательного числа мы получаем очень большое положительное и, соответственно, выход за границы массива. Это тоже UB. {.task_text}
Исправьте этот код, поменяв типы на более подходящие. {.task_text}
```cpp {.task_source #cpp_chapter_0141_task_0050}
class CharTable
{
public:
CharTable()
{
m_is_printable.fill(false);
}
bool is_printable(char c)
{
return m_is_printable[c];
}
private:
std::array<bool, 256> m_is_printable;
};
void test_chartable()
{
char c = 128;
CharTable table;
std::println("character raw val {}", short(c));
std::println("{}", table.is_printable(c));
}
```
Чтобы в переменную `c` корректно записалось значение 128, ее тип нужно сделать беззнаковым. Чтобы метод `is_printable()` был безопасен для всех входных данных, тип принимаемого параметра также должен быть беззнаковым. {.task_hint}
```cpp {.task_answer}
class CharTable
{
public:
CharTable()
{
m_is_printable.fill(false);
}
bool is_printable(unsigned char c)
{
return m_is_printable[c];
}
private:
std::array<bool, 256> m_is_printable;
};
void test_chartable()
{
unsigned char c = 128;
CharTable table;
std::println("character raw val {}", short(c));
std::println("{}", table.is_printable(c));
}
```
Тот факт, что `char` занимает ровно 1 байт, служит отправной точкой для еще нескольких гарантий:
```cpp
1 == sizeof(char) <= sizeof(short) <= sizeof(int) <= sizeof(long) <= sizeof(long long)
```
Типы `char`, `signed char` и `unsigned char` могут вместить любой символ из кодировки [ASCII.](https://en.wikipedia.org/wiki/ASCII) А для работы с семейством UTF предназначены другие типы:
- `wchar_t` (wide character). Имеет тот же размер и наличие знака, что и _один из целочисленных типов_. На практике под Windows `wchar_t` поддерживает UTF-16, имеет ширину 16 бит и используется для работы с [WinAPI](https://en.wikipedia.org/wiki/Windows_API). На остальных системах он может поддерживать UTF-32 и иметь ширину в 32 бита. Так как размер `wchar_t` определяется компилятором, не используйте его для хранения Unicode-текста в кросс-платформенных приложениях.
- `char8_t` — этот тип добавлен в C++20 для работы с UTF-8.
- `char16_t` и `char32_t` — соответственно нужны для работы с UTF-16 и UTF-32.
## Типы вещественных чисел
В C++ для вещественных чисел используется представление с [плавающей точкой.](https://ru.wikipedia.org/wiki/%D0%A7%D0%B8%D1%81%D0%BB%D0%BE_%D1%81_%D0%BF%D0%BB%D0%B0%D0%B2%D0%B0%D1%8E%D1%89%D0%B5%D0%B9_%D0%B7%D0%B0%D0%BF%D1%8F%D1%82%D0%BE%D0%B9) Это компромисс между диапазоном значений и точностью.
Стандарт описывает три типа с плавающей точкой и гарантирует, что каждый последующий имеет размер и точность _не меньшие,_ чем предыдущий:
- `float` — [одинарная](https://en.wikipedia.org/wiki/Single-precision_floating-point_format) точность. Чаще всего занимает 4 байта и обладает точностью около 7 знаков после запятой.
- `double` — [двойная](https://en.wikipedia.org/wiki/Double-precision_floating-point_format) точность. Как правило занимает 8 байт и имеет точность примерно в 15 знаков после запятой.
- `long double` — [расширенная точность.](https://en.wikipedia.org/wiki/Extended_precision) Реализации этого типа сильно разнятся в зависимости от компилятора и целевой архитектуры. Где-то `long double` ничем не отличается от `double`, а где-то занимает 10 байт и обладает точностью примерно 19 знаков. Задумывайтесь об использовании `long double`, если точности `double` не хватает, а целевая платформа поддерживает расширенную точность.
Перечислите через пробел три целых числа: размер типов `float`, `double` и `long double` в байтах, который они принимают в плэйграунде. Для этого откройте [плэйграунд](https://senjun.ru/playground/cpp/) и воспользуйтесь оператором `sizeof`. {.task_text}
```consoleoutput {.task_source #cpp_chapter_0141_task_0060}
```
Пример получения размера типа в байтах: `sizeof(int)`. {.task_hint}
```cpp {.task_answer}
4 8 16
```
### Специфика представления чисел с плавающей точкой
Числа с плавающей точкой имеют важную особенность. Заключается она в том, что на _ограниченный_ набор чисел с плавающей точкой проецируется _бесконечное_ множество вещественных чисел. Из-за этого многие вещественные числа получают неточное представление с плавающей точкой.
Представьте, что бесконечность вещественных чисел проецируется на сетку значений с неравномерным размером ячеек. Эта сетка достаточно плотная для небольших значений и разреженная — для больших. То есть промежутки между соседними числами с плавающей точкой малы, если сами числа малы. И достаточно велики, если числа большие.
Иными словами, на больших значениях абсолютная точность чисел с плавающей точкой _ниже,_ чем на малых. Убедитесь в этом:
```cpp {.example_for_playground .example_for_playground_024}
float a = 123456789.0;
float b = 0.123456789;
std::println("{}\n{}", a, b);
```
```
123456792
0.12345679
```
Сравните погрешность, полученную при сохранении в `float` большого и маленького чисел. Она отличается на десять порядков! Откройте этот пример в плэйграунде и посмотрите, как поведет себя тип для двойной точности `double`.
### Бесконечность и нечисло
У вещественных чисел есть два особых значения:
- `INFINITY` — бесконечность.
- `NAN` — нечисло (not a number). Нечислом может быть результат таких операций как деление на ноль или получение логарифма от числа, меньшего или равного нулю.
У `INFINITY` и `NAN` тип `float`, но оба значения могут быть присвоены и переменной типа `double`. Для их использования нужно подключить хедер [cmath](https://en.cppreference.com/w/cpp/header/cmath.html), даже если импортирован стандартный модуль. Дело в том, что оба значения — это определенные [директивой](/courses/cpp/chapters/cpp_chapter_0100/#block-macro) `#define` макросы, а не полноценные константы, которые могли бы быть экспортированы из модуля.
```cpp {.example_for_playground}
#include <cmath>
import std;
int main()
{
std::println("{} {}", INFINITY, NAN);
}
```
```
inf nan
```
Современный C++ не поощряет использование макросов. Вместо `INFINITY` и `NAN` лучше применять шаблонные функции [std::numeric_limits<T>::infinity()](https://en.cppreference.com/w/cpp/types/numeric_limits/infinity.html) и [std::numeric_limits<T>::quiet_NaN()](https://en.cppreference.com/w/cpp/types/numeric_limits/quiet_NaN.html):
```cpp {.example_for_playground}
import std;
int main()
{
std::println("{} {}",
std::numeric_limits<double>::infinity(),
std::numeric_limits<double>::quiet_NaN());
}
```
```
inf nan
```
В отличие от бесконечности, нечисло _никогда_ не равно самому себе.
```cpp {.example_for_playground .example_for_playground_014}
auto inf = std::numeric_limits<double>::infinity();
auto nan = std::numeric_limits<double>::quiet_NaN();
std::println("{} {}", inf == inf, nan == nan);
```
```
true false
```
Поэтому напрямую сравнивать значения с `NAN` нельзя: любое сравнение вернет `false`. Вместо этого пользуйтесь функцией [std::isnan()](https://en.cppreference.com/w/cpp/numeric/math/isnan.html).
```cpp {.example_for_playground .example_for_playground_015}
if (std::isnan(samples_avg))
std::println("Couldn't calculate average");
```
По аналогии с `std::isnan()` существует функция [std::isinf()](https://en.cppreference.com/w/cpp/numeric/math/isinf.html) для более удобного сравнения с бесконечностью.
### Сравнение вещественных чисел
Будьте осторожны при сравнении чисел с плавающей точкой.
Во-первых, всегда уточняйте, могут ли они по логике программы принимать значения бесконечности и нечисла. Если да, то добавляйте соответствующие проверки через `std::isinf()` и `std::isnan()`.
Во-вторых, из-за специфики представления таких чисел в памяти компьютера их нельзя сравнивать оператором `==` напрямую.
Например, будет ли равно единице значение, полученное прибавлением `0.1` к нулю десять раз?
```cpp {.example_for_playground .example_for_playground_016}
double plan = 1.0;
double delta = 0.1;
double sum = 0.0;
for (auto i = 0; i < 10; ++i)
sum += delta;
std::println("plan == sum : {}\n", plan == sum);
// Выводим plan с точностью в 1 знак после запятой,
// а sum и delta - в 20 знаков.
std::println("plan : {:.1f}\nsum : {:.20f}\ndelta : {:.20f}\n",
plan, sum, delta);
```
```
plan == sum : false
plan : 1.0
sum : 0.99999999999999988898
delta : 0.10000000000000000555
```
Как видите, полученное значение `sum` не равно ожидаемому `plan`.
Если вам известна погрешность, в рамках которой числа считаются равными, то сравнение можно организовать следующим образом:
```cpp {.example_for_playground .example_for_playground_017}
double eps = 1e-4; // погрешность
bool eq = std::abs(a - b) <= eps;
```
Функция [std::abs()](https://en.cppreference.com/w/cpp/numeric/math/abs) возвращает модуль числа.
Такой подход к сравнению сгодится, например, для координат на земном шаре. Если две широты и долготы совпадают друг с другом вплоть до 7-го знака, то координаты считаются совпадающими.
Напишите функцию `is_eq()`, которая проверяет числа `a` и `b` на равенство с точностью до `eps` включительно. Функция шаблонная, и ее параметры могут иметь любой тип, описывающий вещественные числа. {.task_text}
Если хотя бы одно из чисел равно бесконечности или нечислу, функция должна вернуть `false`. {.task_text}
```cpp {.task_source #cpp_chapter_0141_task_0070}
template<class T>
bool is_eq(T a, T b, T eps)
{
}
```
Вызовите `std::isinf()` и `std::isnan()` для сравнения чисел с бесконечностью и нечислом. {.task_hint}
```cpp {.task_answer}
template<class T>
bool is_eq(T a, T b, T eps)
{
if (std::isinf(a) || std::isinf(b) || std::isnan(a) || std::isnan(b))
return false;
return std::abs(a - b) <= eps;
}
```
Если значения колеблются в широком диапазоне, то погрешность `eps` становится частью данных. Вы должны вычислять ее в зависимости от величин сравниваемых чисел. Взгляните на способ, который хорош для сравнения чисел одинакового знака. В нем используется функция `std::numeric_limits<T>::epsilon()`, которая возвращает _машинный эпсилон_ — разность между `1.0` и следующим за ним числом типа `T`:
```cpp {.example_for_playground .example_for_playground_018}
double abs_max = std::max(std::abs(a), std::abs(b));
double eps = std::numeric_limits<double>::epsilon() * abs_max;
bool eq = std::abs(a - b) <= eps;
```
Приведенный способ не годится, если одно из чисел отрицательное, а другое — положительное. К тому же, при сложных вычислениях может накапливаться погрешность, из-за которой константа `std::numeric_limits<T>::epsilon()` окажется слишком маленькой и перестанет подходить.
Универсального способа для сравнения чисел с плавающей точкой просто не существует. Подбирайте его, отталкиваясь от природы данных. И помните, что зачастую погрешность сравнения должна вычисляться в зависимости от величин сравниваемых значений, а не быть константой.
## Псевдонимы типов {#block-alias}
В C++ можно задавать псевдоним (alias) для уже существующего типа. В стандартной библиотеке определено множество псевдонимов фундаментальных типов, облегчающих жизнь разработчика.
### Ключевые слова typedef и using
Псевдонимы создаются двумя способами: через спецификатор `typedef` и ключевое слово `using`. У `using` есть и другие применения, о которых вы скоро узнаете.
В общем виде объявление псевдонима через [typedef](https://en.cppreference.com/w/cpp/language/typedef.html) выглядит так:
```cpp
typedef source_type alias;
```
Пример:
```cpp {.example_for_playground}
import std;
typedef int RetCode;
typedef unsigned long Index;
int main()
{
const RetCode res = -1;
const Index batch_idx = 135450;
std::println("Batch #{}. RetCode: {}", batch_idx, res);
}
```
```
Batch #135450. RetCode: -1
```
Более современный вариант задания псевдонима — через ключевое слово [using](https://en.cppreference.com/w/cpp/language/type_alias.html). Семантически этот способ [не отличается](https://timsong-cpp.github.io/cppwp/dcl.typedef#2) от `typedef`, просто имеет другой синтаксис:
```cpp
using alias = source_type;
```
Определим псевдоним `UserId`, а на базе него — псевдоним `UserIdArr`:
```cpp {.example_for_playground .example_for_playground_019}
using UserId = short;
using UserIdArr = std::vector<UserId>;
UserIdArr get_blocked_users()
{
// ...
}
```
Как правило, в новом коде предпочтение отдают варианту с `using`, а `typedef` оставляют для фрагментов, которые должны быть совместимы с Си. Объявление псевдонимов через `using` более читабельное. На простых типах это незаметно, поэтому разницу лучше продемонстрировать на шаблонных классах:
```cpp
typedef std::map<std::string, std::vector<std::pair<int, int>> my_map;
```
```cpp
using my_map = std::map<std::string, std::vector<std::pair<int, int>>;
```
Псевдонимы полезны для абстрагирования логики проекта от конкретных типов. Допустим, в коде приложения активно используется такая сущность как id транзакции. Изначально предполагалось, что в качестве типа сгодится `int`. Но требования поменялись, и тип нужно заменить на `unsigned long long`. Без псевдонима пришлось бы найти _все_ объявления переменных этого типа, и поменять его. А с псевдонимом замена делается единожды, по месту его объявления.
Вторая причина для введения псевдонимов — более ясный код.
```cpp
// Непонятно, что возвращает функция:
// код возврата, id процесса, количество
// обработанных байт, что-то еще?
unsigned int run_task();
```
Сравните:
```cpp
using ProcessId = unsigned int;
ProcessId run_task();
```
Псевдонимы хорошо ложатся на решение [практики](/courses/cpp/practice/cpp_lru_cache/) «LRU кеш», в котором нужно работать со списком пар ключ-значение и словарем, в котором значение — итератор на элемент этого списка:
```cpp
std::list<std::pair<int, std::string>> list;
std::unordered_map<int, std::list<std::pair<int, std::string>>::iterator> map;
```
Добавление псевдонима _хотя бы_ для пары ключ-значение уже делает код более читабельным:
```cpp
using KeyVal = std::pair<int, std::string>;
std::list<KeyVal> list;
std::unordered_map<int, std::list<KeyVal>::iterator> map;
```
### Целочисленные типы фиксированной ширины
Стандарт не определяет точную ширину целочисленных типов. Размер таких типов как `int`, `short`, `long long` зависит от реализации. Однако в ряде случаев гарантия конкретного размера просто необходима. Например, это важно при [сериализации](https://ru.wikipedia.org/wiki/%D0%A1%D0%B5%D1%80%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B0%D1%86%D0%B8%D1%8F) данных, когда набор значений превращается в поток подряд идущих байт. Затем этот поток пересылается по сети или сохраняется в файл. Чтобы прочитать его, нужно знать, в каком порядке идут значения и сколько бит они занимают. Если при записи `int` занимал 64 бита, а при чтении — 32, то мы не сможем корректно извлечь данные.
На помощь приходят [типы фиксированной ширины:](https://en.cppreference.com/w/cpp/types/integer.html)
- `std::int8_t`, `std::int16_t`, `std::int32_t`, `std::int64_t` — знаковые целые размером 8, 16, 32 и 64 бита.
- `std::uint8_t`, `std::uint16_t`, `std::uint32_t`, `std::uint64_t` — их беззнаковые вариации.
```cpp {.example_for_playground .example_for_playground_020}
std::uint32_t fingerprint32(std::string data)
{
// ...
}
std::uint64_t fingerprint64(std::string data)
{
// ...
}
```
Под капотом это всего лишь псевдонимы фундаментальных типов, подбираемые в зависимости от системы таким образом, чтобы тип точно попадал в заявленный размер. В реализации стандартной библиотеки это может выглядеть примерно так:
```cpp
namespace std
{
typedef unsigned char uint8_t;
typedef unsigned int uint32_t;
typedef signed short int int16_t;
// ...
}
```
Жизненный пример использование типа `std::uint32_t` — для хранения [адресов IPv4,](https://en.wikipedia.org/wiki/IPv4#Addressing) например `127.0.0.1`. Каждый из 4-х октетов адреса принимает значение от `0` до `255` и занимает ровно байт. Первый октет сохраняется в в старший байт `std::uint32_t`, последний — в младший.
Поясним это на примере преобразования адреса `172.16.254.1` в число 2886794753. Так оно выглядит в двоичном виде:
 {.illustration}
Напишите функцию `convert_ip_v4()`, которая принимает ip-адрес в виде строки, разделенной точками на октеты. Функция должна вернуть целочисленное представление адреса. {.task_text}
Вам помогут: {.task_text}
- Функция [std::stoi()](https://en.cppreference.com/w/cpp/string/basic_string/stol), которая конвертирует строку в число.
- Оператор `<<` для [побитового сдвига](/courses/cpp/practice/cpp_div_without_div/#block-bitwise) и оператор `|` для [побитового «ИЛИ».](https://en.cppreference.com/w/cpp/language/operator_arithmetic.html)
```cpp {.task_source #cpp_chapter_0141_task_0080}
std::uint32_t convert_ip_v4(std::string ip)
{
}
```
Воспользуйтесь методом `find()` строки, чтобы определить в ней индексы точек. Точки разделяют октеты адреса. Получите значение каждого из октетов методом строки `substr()`. Переведите их из строки в число функцией `std::stoi()`. Заведите переменную для десятичного представления ip-адреса и проинициализируйте ее нулем. Затем заполните ее четырьмя октетами. Добавление к число октета означает сдвиг этого числа на 8 бит влево и заполнение младших 8-ми бит значением октета. {.task_hint}
```cpp {.task_answer}
// Добавляет к числу ip октет из строки ip_str, ограниченный индексами
std::uint32_t add_octet(std::uint32_t ip,
std::string ip_str,
std::size_t i_start,
std::size_t i_end)
{
const std::string octet_str = ip_str.substr(i_start, i_end - i_start);
const std::uint32_t octet = std::stoi(octet_str);
// Сдвигает число ip на 8 бит влево и заполняет младшие 8 бит
// числом octet
return (ip << 8) | octet;
}
std::uint32_t convert_ip_v4(std::string ip)
{
std::uint32_t ip_int = 0;
std::size_t start = 0;
std::size_t end_i = ip.find('.');
while (end_i != std::string::npos)
{
ip_int = add_octet(ip_int, ip, start, end_i);
start = end_i + 1;
end_i = ip.find('.', start);
}
ip_int = add_octet(ip_int, ip, start, ip.size());
return ip_int;
}
```
### Тип size_t
С типом `std::size_t` вы уже [знакомы.](/courses/cpp/chapters/cpp_chapter_0020/#block-size_t) Это беззнаковое целое, которое может хранить индекс элемента контейнера, его длину, счетчик цикла, размер переменной.
Пора узнать, что на самом деле `std::size_t` — всего лишь псевдоним над одним из беззнаковых целых. Оно выбирается таким образом, чтобы вместить размер любого объекта в байтах. Ведь оператор [sizeof](https://en.cppreference.com/w/cpp/language/sizeof.html) возвращает именно тип `std::size_t`.
В старом коде вы можете заметить, что вместо `std::size_t` часто используется `int`. В современном C++ это считается плохой практикой, потому что приводит к беззнаковому переполнению на больших значениях.
```cpp
std::vector<std::string> v = read_data();
// Если длина v превысит максимальное для int
// значение, будет переполнение
for(int i = 0; i < v.size(); ++i)
{
std::println("{} {}", i, v[i]); // UB при переполнении
}
```
Циклы с `int` в качестве счетчика попадаются и в новом коде. Так выглядит ненадежный подход к итерированию в обратном порядке. Ненадежный — опять же из-за возможного переполнения на больших значениях счетчика.
```cpp
std::vector<std::string> v = read_data();
// Когда i станет равным -1, произойдет выход из цикла
for(int i = v.size(); i >= 0; --i)
{
std::println("{} {}", i, v[i]);
}
```
Просто заменить `int` на `std::size_t` в этом примере нельзя, ведь декремент нуля приведет к переполнению снизу. Вслед за нулем счетчик цикла примет максимальное для `std::size_t` значение.
Чтобы проитерироваться в обратном порядке, вместо счетчика типа `int` вспомните про [обратные итераторы.](/courses/cpp/chapters/cpp_chapter_0060/#block-reverse-iterators) Другой вариант — библиотека [ranges](https://en.cppreference.com/w/cpp/ranges.html) (диапазоны), появившаяся в C++20. В этом курсе мы ее тоже рассмотрим.
Перед вами функция `build_inverted_index()`, которая принимает коллекцию отсортированных по возрастанию популярности документов. Функция строит по ним [обратный индекс](https://ru.wikipedia.org/wiki/%D0%98%D0%BD%D0%B2%D0%B5%D1%80%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%BD%D1%8B%D0%B9_%D0%B8%D0%BD%D0%B4%D0%B5%D0%BA%D1%81). В данном случае это просто словарь, соотносящий имени документа его позицию в коллекции. Если в ней встречаются документы с одинаковым именем, то в словарь попадает только самый популярный из них. {.task_text}
Функция реализована циклом со счетчиком типа `int` при обработке больших коллекций вызывает UB. Перепишите ее через обратные итераторы и замените тип индекса документа с `int` на `std::size_t`. {.task_text}
Вам помогут: {.task_text}
- Функция [std::distance()](/courses/cpp/chapters/cpp_chapter_0060/#block-distance).
- Метод [base()](/courses/cpp/chapters/cpp_chapter_0060/#block-base) обратного итератора.
```cpp {.task_source #cpp_chapter_0141_task_0100}
using DocName = std::string;
using DocIdx = int;
struct Doc
{
DocName name;
std::string text;
};
std::unordered_map<DocName, DocIdx> build_inverted_index(std::vector<Doc> docs)
{
std::unordered_map<DocName, DocIdx> inverted_index;
for(int i = docs.size(); i >= 0; --i)
{
inverted_index.try_emplace(docs[i], i);
}
return inverted_index;
}
```
Будьте внимательны: метод `base()` обратного итератора возвращает обычный итератор. Он на один элемент ближе к концу контейнера, чем обратный. {.task_hint}
```cpp {.task_answer}
using DocName = std::string;
using DocIdx = std::size_t;
struct Doc
{
DocName name;
std::string text;
};
std::unordered_map<DocName, DocIdx> build_inverted_index(std::vector<Doc> docs)
{
std::unordered_map<DocName, DocIdx> inverted_index;
for (auto rit = docs.rbegin(); rit != docs.rend(); ++rit)
{
const std::size_t i = std::distance(docs.begin(), rit.base()) - 1;
inverted_index.try_emplace(rit->name, i);
}
return inverted_index;
}
```
## Суффиксы литералов
У любого литерала в C++ есть тип по умолчанию, например:
```cpp {.example_for_playground .example_for_playground_021}
auto skip_dependencies = false; // bool
auto raw_estimate_s = 11.4; // double
```
Для целочисленных литералов подбирается наименьший из типов, способный вместить значение. Выбор происходит между `int`, `long`, `long long` и их беззнаковыми вариациями.
```cpp {.example_for_playground .example_for_playground_022}
auto zero = 0; // int
auto bitrate_kbps = 5'800; // int
auto seed = 1844674407370999999; // long
```
Порой тип литерала требуется поменять. Вы уже столкнулись с этим в [практике](/courses/cpp/practice/cpp_div_without_div/) «Деление без деления» при сдвиге значения `1` на `n` бит влево. При этом единица должна была интерпретироваться как литерал типа `std::size_t`, а не `int`.
```cpp
1 << n // 1 - это int
```
Можно было бы воспользоваться [прямой](/courses/cpp/chapters/cpp_chapter_0131/#block-direct-initialization) или [универсальной](/courses/cpp/chapters/cpp_chapter_0131/#block-uniform-initialization) инициализацией:
```cpp
std::size_t(1) << n // прямая инициализация
std::size_t{1} << n // универсальная
```
Но вместо этого мы выбрали более лаконичный способ — добавили к литералу суффикс `uz`, который был введен в C++23 специально для обозначения `std::size_t`:
```cpp
1uz << n // 1 - это std::size_t
```
Всего есть три суффикса:
- `l` или `L` — добавление к типу модификатора `long`.
- `u` или `U` — выбор беззнаковой вариации типа.
- `z` или `Z` — выбор знаковой вариации типа, псевдонимом которого является `std::size_t`.
Суффиксы можно комбинировать между собой, например суффикс `LL` добавляет к типу модификатор `long long`.
Какой суффикс нужно добавить к целочисленному литералу, чтобы получить беззнаковое целое шириной от 64 бита? {.task_text}
```consoleoutput {.task_source #cpp_chapter_0141_task_0090}
```
Целевой тип — `unsigned long long`. {.task_hint}
```cpp {.task_answer}
ull
```
## Какой тип выбрать?
Напоследок несколько советов по работе с фундаментальными типами:
- Старайтесь не смешивать знаковые и беззнаковые типы в арифметических выражениях. Из-за [неявного приведения типов](/courses/cpp/chapters/cpp_chapter_0131/#block-implicit-conversion) в таком коде легко допустить ошибку.
- Используйте типы фиксированной ширины, если критичен точный размер переменной. В остальных случаях предпочитайте обычные типы.
- Подбирайте тип таким образом, чтобы не допустить переполнения.
## Домашнее задание
Откройте свое решение [практики](/courses/cpp/practice/cpp_lru_cache/) «LRU кеш» и добавьте в него псевдонимы. Сравните старый и новый варианты кода: стало ли решение более лаконичным и понятным?
----------
## Резюме
- Ширина целочисленного типа — это размер, занимаемый переменными этого типа в памяти.
- Модификаторы `signed`, `unsigned`, `short` и `long` нужны, чтобы на базе `int` выводить другой целочисленный тип.
- Стандарт не определяет точную ширину типа `int` и его производных, полученных с помощью модификаторов.
- Переполнение _знакового_ целого типа — это UB. Переполнение _беззнакового_ приводит к отбрасыванию старших битов.
- Оператор `sizeof` возвращает размер типа в байтах.
- Псевдонимы `std::int8_t`, ..., `std::int64_t` и их беззнаковые вариации определяют целочисленные типы фиксированной ширины.
- Ключевые слова `typedef` и `using` задают псевдоним уже существующего типа.
- Префиксы целочисленных литералов нужны, чтобы указать систему счисления. Например, `0x` для шестнадцатеричного значения.
- Суффиксы `l`, `u`, `z` или их сочетание явно задают тип литерала.
Следующие главы находятся в разработке
Наша группа в telegram. Здесь можно задавать вопросы и общаться.
Задонатить. Если вам нравится курс, вы можете поддержать развитие площадки!