﻿# Путеводитель C\+\+ программиста по неопределённому поведению: часть 2 из 11

Вашему вниманию предлагается вторая часть электронной книги, которая посвящена неопределённому поведению\. Книга не является учебным пособием и рассчитана на тех, кто уже хорошо знаком с программированием на C\+\+\. Это своего рода путеводитель C\+\+ программиста по неопределённому поведению, причём по самым его тайным и экзотическим местам\. Автор книги — Дмитрий Свиридкин, редактор — Андрей Карпов\.

![1136_book_pt_2_ru/image1.png](https://import.viva64.com/docx/blog/1136_book_pt_2_ru/image1.png)

## Целые и вещественные числа: переполнение целых знаковых чисел

Большая часть написанного и ещё не написанного кода любой программы так или иначе работает с числами\. Вычисление по каким\-либо формулам, увеличение или уменьшение счётчиков итераций циклов, рекурсивных вызовов, элементов контейнеров — работа с числами везде\.

Компьютер не может напрямую работать с бесконечно "длинными" числами — хранить все их цифры\. Как бы много оперативной памяти у нас ни было, всё же она конечна\. Да и хранить, и обрабатывать величины, сопоставимые с числом атомов в видимой части Вселенной — безнадёжное занятие\.

Тем не менее при выполнении операций над целыми числами мы всё же имеем шанс выпасть за пределы допустимого диапазона \(например, _\[\-2^31, 2^31\-1\]_ для _int32_\)\. И тут в игру вступают особенности поддержки целых чисел для того или иного языка программирования, а также, быть может, особенности реализации конкретной платформы\.

При выполнении инструкции _add_ \(_iadd_\) платформы х86 переполнение целого числа сопровождается выставлением специального флага переполнения, а результирующее значение просто получается отбрасыванием старшего бита результата\. И следует ожидать, что по окончании работы условной программы:

```cpp
x = 2^31 - 1
iadd x 5
```

произойдёт перенос разряда в знаковый бит, и переменная _x_ примет отрицательное значение\.

В реализации конкретного языка программирования может быть проверка флага переполнения и сообщение об ошибке\. А может и не быть\. Может быть гарантия "цикличности" значений \(после _2^31\-1_ идёт _\-2^31_\), а может и не быть\.

Проверки и гарантии — это дополнительные инструкции, которые нужно генерировать компилятору, а процессору потом исполнять\.

В языке C\+\+ решили не жертвовать производительностью и заставлять компиляторы генерировать код проверки, а объявили переполнение целых знаковых \(_signed_\) чисел неопределённым, открывая простор для оптимизаций\. Компилятор может генерировать любой код, какой ему вздумается, ориентируясь лишь на одно правило: переполнения не бывает\.

Многие программисты свято верят, что переполнение чисел работает, как ожидается, "циклично", и пишут проверки вида:

```cpp
if (x > 0 && a > 0 && x + a <= 0) {
    // обработай переполнение
}
```

Но, увы, это неопределённое поведение\. И компилятор имеет полное право [выкинуть](https://godbolt.org/z/dhs83T) такую проверку\.

<details>
   <summary>Пример генерации кода компилятором GCC 10\\\.2 для x86\\\-64 \\\(\\\-std\\\=c\\\+\\\+14 \\\-O3\\\)\\\.</summary>

```cpp
int main() {
    int x = 2'000'000'000;
    int y = 0;
    std::cin >> y;

    if (x > 0 && y > 0 && x + y <=0){
        return 5;
    }
    return 0;
}
```

Обратите внимание, что в ассемблерном коде после вызова функции чтения из потока \(_call_\) сразу следует обнуление регистра _eax_ \(_xor eax, eax_\) и возвращение его как результата функции\.

```cpp
main:
 sub     rsp, 24
 mov     edi, OFFSET FLAT:std::cin
 lea     rsi, [rsp+12]
 mov     DWORD PTR [rsp+12], 0
 call    std::basic_istream<char, std::char_traits<char> >::operator>>(int&)
 xor     eax, eax
 add     rsp, 24
 ret
```


</details>
Искусственный пример может быть недостаточно убедительным, так что обратим внимание на следующую — вполне серьёзную — функцию вычисления полиномиального хэша строки:

```cpp
int hash_code(std::string s) {
    int h = 13;
    for (char c : s) {
        h += h * 27752 + c;
    }
    if (h < 0) h += std::numeric_limits<int>::max();
    return h;
}
```

Функция, которая по задумке никогда не должна возвращать отрицательные числа, таки [выдаёт](https://godbolt.org/z/4v139E) отрицательное число\! Из\-за неопределённого поведения и бессмысленной с точки зрения компилятора проверки\.

Компилятор может руководствоваться следующей логикой:

1. Если значение _h_ положительно, то независимо от символа _c_ величина _h\*27752 \+ с_ будет положительной: величина _c_ мала, а переполнения не бывает\.
1. На первой итерации _h_ положительно, мы суммируем положительные числа, переполнений в корректной программе не бывает, значит на каждой итерации значение будет оставаться положительным\.
1. Конечная сумма в итоге должна получиться положительной, и проверка не нужна\.

Другой замечательный, но искусственный пример, для большего устрашения: [конечный цикл может стать бесконечным](https://godbolt.org/z/Y6bTP3MK3)\! Пример взят из публикации "[Shocking Examples of Undefined Behaviour](https://mohitmv.github.io/blog/Shocking-Undefined-Behaviour-In-Action/)":

```cpp
int main() {
  char buf[50] = "y";
  for (int j = 0; j < 9; ++j) {
    std::cout << (j * 0x20000001) << std::endl;
    if (buf[0] == 'x') break;
  }
}
```

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

```cpp
for(int j = 0; j < 9*0x20000001; j += 0x20000001) {
  ....
}
```

Условие _j < 9\*0x20000001_ всегда истинно, так как правая часть больше, чем _std::numeric\_limits<int\>::max\(\)_\.

С современными версиями компиляторов этот пример особенно занятен\. GCC в подобных циклах иногда способны заметить переполнение и выдать предупреждение\. Но этого не произошло\.\.\. Однако если мы закомментируем недостижимый _break_ и _buf_, мы [получим](https://godbolt.org/z/Wszeb4a6s) предупреждение:

```cpp
<source>:6:37: warning:
iteration 4 invokes undefined behavior [-Waggressive-loop-optimizations]
    6 |         std::cout << (j * 0x20000001) << std::endl;
      |                                     ^
<source>:5:23: note: within this loop
    5 |     for (int j = 0; j < 9; ++j) {
```

Если раскомментировать объявление _buf_, то предупреждение [пропадёт](https://godbolt.org/z/cK483MnP3) \(GCC 13\.2\)\.

Бывает и наоборот\. Ждёшь последствия от переполнения, а его нет, и код магическим образом работает\. Пример из статьи "[Undefined behavior ближе, чем вы думаете](https://pvs-studio.ru/ru/blog/posts/cpp/0374/)":

```cpp
size_t Count = size_t(5) * 1024 * 1024 * 1024; // 5 Gb
char *array = (char *)malloc(Count);
memset(array, 0, Count);

int index = 0;
for (size_t i = 0; i != Count; i++)
  array[index++] = char(i) | 1;
```

Инкрементируясь, 32\-битная знаковая переменная _index_ в какой\-то момент переполнится и, кажется, должна стать отрицательной\. После чего произойдёт Access Violation при выходе за границу массива\. Но в случае UB никто никому ничего не должен\.

Компилятор решает в целях оптимизации использовать для переменной _index_ 64\-битный регистр, который отлично увеличивается, и все элементы массива успешно заполняются\. И он в своём праве: если переполнение не должно возникать, то и использовать 32\-битный регистр для индекса он не обязан\.

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

Например, при суммировании арифметических прогрессий и некоторых других известных рядов Clang 12 [генерирует](https://godbolt.org/z/oE7q6WjTv) совершенно разный код для знаковых и беззнаковых чисел\.

Вариант со знаковыми типами:

```cpp
// суммируем квадраты от 1 до N
int64_t summate_squares(int64_t n) {
    int64_t sum = 0;
    for (int64_t i = 1; i <= n; ++i) {
        sum += i * i;
    };
    return sum;
}
```

Ассемблерный листинг \(x86\-64 clang 12\.0\.1, \-std\=c\+\+20 \-O3\)\. Обратите внимание, что здесь нет цикла\. Используется известная формула _\(N \* \(N \+ 1\)\) \* \(2N \+ 1\) / 6_, но довольно сложным способом:

```cpp
summate_squares(long):                   # @summate_squares(long)
        test    rdi, rdi
        jle     .LBB2_1
        lea     rax, [rdi - 1]
        lea     rcx, [rdi - 2]
        mul     rcx
        mov     r8, rax
        mov     rsi, rdx
        lea     rcx, [rdi - 3]
        mul     rcx
        imul    ecx, esi
        add     edx, ecx
        shld    rdx, rax, 63
        movabs  rax, 6148914691236517206
        shld    rsi, r8, 63
        imul    rax, rdx
        lea     rcx, [rsi + 4*rsi]
        add     rcx, rax
        lea     rax, [rcx + 4*rdi]
        add     rax, -3
        ret
.LBB2_1:
        xor     eax, eax
        ret
*/
```

Вариант с беззнаковыми типами:

```cpp
uint64_t usummate_squares(uint64_t n) {
    uint64_t sum = 0;
    for (uint64_t i = 1; i <= n; ++i) {
        sum += i * i;
    };
    return sum;
}
```

Здесь цикл есть\. Переполнение беззнаковых типов определено и требует обработки:

```cpp
usummate_squares(unsigned long):       # @usummate_squares(unsigned long)
        test    rdi, rdi
        je      .LBB3_1
        mov     ecx, 1
        xor     eax, eax
.LBB3_4:                               # =>This Inner Loop Header: Depth=1
        mov     rdx, rcx
        imul    rdx, rcx
        add     rax, rdx
        add     rcx, 1
        cmp     rcx, rdi
        jbe     .LBB3_4
        ret
.LBB3_1:
        xor     eax, eax
        ret
```

GCC 13 на момент написания текста \(2024 год\) в принципе [не делает](https://godbolt.org/z/xjf7zj768) таких оптимизаций по умолчанию\. При этом последние версии Clang 18 уже [способны](https://godbolt.org/z/WqaeaPjfe) свернуть цикл суммирования квадратов и для беззнаковых:

```cpp
usummate_squares(unsigned long):       # @usummate_squares(unsigned long)
        test    rdi, rdi
        je      .LBB3_1
        inc     rdi
        cmp     rdi, 3
        mov     r8d, 2
        cmovae  r8, rdi
        lea     rax, [r8 - 2]
        lea     rcx, [r8 - 3]
        mul     rcx
        mov     rsi, rax
        mov     rcx, rdx
        lea     rdi, [r8 - 4]
        mul     rdi
        imul    edi, ecx
        add     edx, edi
        shld    rdx, rax, 63
        movabs  rax, 6148914691236517206
        shld    rcx, rsi, 63
        imul    rax, rdx
        lea     rcx, [rcx + 4*rcx]
        add     rcx, rax
        lea     rax, [rcx + 4*r8]
        add     rax, -7
        ret
.LBB3_1:
        xor     eax, eax
        ret
```

_Читатели, искушённые в теории колец вычетов, могут для беззнаковой версии написать более простой и короткий ассемблерный код в качестве упражнения \(нужно лишь правильно поделить на 6\)\._

Корректные проверки переполнения в арифметических операциях намного сложнее, чем просто смена знака\.

Так, для C\+\+20 безопасный обобщённый код арифметических операций над целыми знаковыми числами мог бы выглядеть следующим образом:

```cpp
#include <concepts>
#include <type_traits>
#include <variant>
#include <limits>

namespace safe {

// Все эти проверки справедливы только для целых знаковых чисел
template <class T>
concept SignedInteger = std::is_signed_v<T>
                     && std::is_integral_v<T>;

enum class ArithmeticError {
    Overflow,
    ZeroDivision
};

template <SignedInteger I>
using ErrorOrInteger = std::variant<I, ArithmeticError>;

template <SignedInteger I>
ErrorOrInteger<I> add(I a,    // выключаем вывод параметра шаблона по
                      std::type_identity_t<I> b) // второму аргументу
{
    if (b > 0 && a > std::numeric_limits<I>::max() - b) {
        // положительное переполнение
        return ArithmeticError::Overflow;
    }
    if (b < 0 && a < std::numeric_limits<I>::min() - b) {
        // отрицательное переполнение
        return ArithmeticError::Overflow;
    }
    return a + b;
}

template <SignedInteger I>
ErrorOrInteger<I> sub(I a, std::type_identity_t<I> b) {
    if (b < 0 && a > std::numeric_limits<I>::max() + b) {
        // положительное переполнение
        return ArithmeticError::Overflow;
    }
    if (b > 0 && a < std::numeric_limits<I>::min() + b) {
        // отрицательное переполнение
        return ArithmeticError::Overflow;
    }
    return a - b;
}

template <SignedInteger I>
ErrorOrInteger<I> mul(I a, std::type_identity_t<I> b) {
   if (a == 0 || b == 0) {
       return 0;
   }

   if (a > 0) {
       if (b > 0) {
           if (a > std::numeric_limits<I>::max() / b) {
              return ArithmeticError::Overflow;
           }
       } else {
           if (b < std::numeric_limits<I>::min() / a) {
              return ArithmeticError::Overflow;
            }
      }
   } else {
      if (b > 0) {
          if (a < std::numeric_limits<I>::min() / b) {
              return ArithmeticError::Overflow;
          }
      } else {
          if (b < std::numeric_limits<I>::max() / a) {
              return ArithmeticError::Overflow;
          }
      }
   }
   return a * b;
}

template <SignedInteger I>
ErrorOrInteger<I> div(I a, std::type_identity_t<I> b) {
  if (b == 0) {
      return ArithmeticError::ZeroDivision;
  }

  if (a == std::numeric_limits<I>::min() && b == -1) {
      // диапазон [min, max] несимметричный относительно 0.
      // abs(min) > max — будет переполнение
      return ArithmeticError::Overflow;
  }
  return a / b;
}


template <SignedInteger I>
ErrorOrInteger<I> mod(I a, std::type_identity_t<I> b) {
  if (b == 0) {
      return ArithmeticError::ZeroDivision;
  }

  if (b == -1) {
      // По стандарту в этом случае также неопределенное поведение при
      // a == std::numeric_limits<I>::min()
      // поскольку остаток и неполное частное от деления,
      // например, на платформе x86
      // получаются одной и той же инструкцией div (idiv),
      // что потребует дополнительной обработки.
      //
      // Но совершенно ясно, что остаток от деления чего угодно на -1 равен 0
      return 0;
  }
  return a % b;
}

}
```

Если вам не нравится возвращать ошибку или результат, можете использовать исключения\.

Видно, что безопасные версии арифметических операций должны быть как минимум в два раза медленнее своих исходно небезопасных версий\. Такая экономия тактов может быть оправдана, если вы разрабатываете, например, математическую библиотеку, и вся ваша производительность упирается в CPU и перемалывание чисел\.

Однако если ваша программа только и делает, что ожидает и выполняет IO операции, то траты в два раза большего числа тактов на сложение или умножение никто и не заметит\. Да и язык C\+\+ для таких программ чаще всего не лучший выбор\.

Итак, если вы работаете только лишь с беззнаковыми числами \(_unsigned_\), то с неопределённым поведением при переполнении никаких проблем нет: всё определено как вычисления по модулю _2^N_ \(_N_ — количество бит для выбранного типа чисел\)\.

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

Для выведения ограничений вам помогут отладочные _assert_ с правильными проверками переполнения, которые нужно написать\. Или включение _ubsan_ \(_undefined behavior sanitizer_\) при сборке компиляторами Clang или GCC\. А также тестовые _constexpr_ вычисления\.

Также проблемы неопределённого поведения при переполнении касаются битовых сдвигов влево для отрицательных чисел \(или при сдвиге положительного числа с залезанием в знаковый бит\)\. Начиная с C\+\+20, стандарт требует фиксированной единой реализации отрицательных чисел — через дополнительный код \([two's complement](https://en.wikipedia.org/wiki/Two%27s_complement)\), и многие проблемы сдвигов сняты\. Тем не менее всё равно стоит следовать общей рекомендации: любые битовые операции выполнять только в _unsigned_ типах\.

<details>
   <summary>Дополнительный код \\\(two's complement\\\)</summary>

Дополнительный код — наиболее распространённый способ представления отрицательных целых чисел в компьютерах\. Он позволяет заменить операцию вычитания на операцию сложения и сделать операции сложения и вычитания одинаковыми для знаковых и беззнаковых чисел\.

Дополнительный код для отрицательного числа можно получить инвертированием его двоичного модуля \(получается "первое дополнение"\) и прибавлением к инверсии единицы \(получается "второе дополнение"\)\.

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




</details>
Стоит заметить, что сужающее преобразование из целочисленного типа в другой целочисленный тип к неопределённому поведению не приводит, и выполнять побитовое _и_ с маской перед присваиванием переменной меньшего типа необязательно\. Но желательно, чтобы избежать предупреждений компилятора:

```cpp
constexpr int x = 12345678;
constexpr uint8_t first_byte = x; // Implicit cast. Warning
```

Очень неприятным является переполнение целочисленных переменных, возникающее из\-за правил _integer promotion_:

```cpp
constexpr std::uint16_t IntegerPromotionUB(std::uint16_t x) {
    x *= x;
    return x;
}

// 65535 * 65535 mod 1<<16 = 1

static_assert(IntegerPromotionUB(65535) == 1); // won't compile
```

Несмотря на то, что для беззнаковых типов переполнение определено как взятие остатка по модулю _2^n_, и мы используем только беззнаковую переменную, из\-за _integer promotion_ в этом [примере](https://godbolt.org/z/GWsaGo) возникает переполнение знакового \(\!\) числа и вытекающее из этого UB\.

Справедливости ради надо заметить, что такое происходит только на платформах, где размер _int_ больше _uint16\_t_ \(то есть практически везде в наши дни\)\.

```cpp
x *= x; // переписывается как x = x * x;
```

Тип _uint16_ меньше, чем тип _int\. _Для умножения выполняется неявное приведение к _int_\.

#### Полезные ссылки

1. SEI CERT C Coding Standard\. [INT32\-C\. Ensure that operations on signed integers do not result in overflow](https://wiki.sei.cmu.edu/confluence/display/c/INT32-C.+Ensure+that+operations+on+signed+integers+do+not+result+in+overflow)\.
1. CWE\. [CWE\-190: Integer Overflow or Wraparound](https://cwe.mitre.org/data/definitions/190.html)\.
1. Stack Overflow\. [Ответ по теме "Implicit type promotion rules"](https://stackoverflow.com/questions/46073295/implicit-type-promotion-rules/46073296#46073296)\.
1. Воробьёв Никита\. [Как переменная может быть не равной её собственному значению](https://habr.com/ru/articles/307702/)\.
1. Андрей Карпов\. [60 антипаттернов для С\+\+ программиста](https://pvs-studio.ru/ru/blog/posts/cpp/1053/) \(вредный совет N11: Во всём виноват компилятор\)\.

## Целые и вещественные числа: числа с плавающей точкой

С _float_ и _double_ в принципе всегда всё сложно\. Особенно в C\+\+\.

Стандарт C\+\+ не требует следования стандарту IEEE 754, потому деление на ноль в вещественных числах также считается неопределённым поведением несмотря на то, что по IEEE 754 выражение _x/0\.0_ определяется как _\-INF_, _NaN_, или _INF_ в зависимости от знака числа _x_ \(_NaN_ для нуля\)\.

Сравнение вещественных чисел — излюбленная головная боль\.

Выражение _x \=\= y_ фактически является кривым побитовым сравнением для чисел с плавающей точкой, по\-особенному работающее со случаями _\-0\.0_ и _\+0\.0_, и _NaN_\. О существовании этого и _\!\=_ операторов для вещественных чисел стоит забыть и никогда не вспоминать\.

На тот случай, если вам по наследству достался большой проект, и хочется узнать, как в нём обстоит дело со сравнением чисел с плавающей точкой, вы можете воспользоваться анализатором PVS\-Studio\. В нём есть диагностика [V550](https://pvs-studio.ru/ru/docs/warnings/v550/): Suspicious precise comparison\.

Для побитового сравнения нужно использовать _memcmp_\. Для сравнения чисел — приближенные варианты вида _std::abs\(x \- y\) < EPS_, где _EPS_ — какое\-то абсолютное или вычисляемое на основе _x_ и _y_ значение\. А также различные манипуляции с [ULP](https://en.wikipedia.org/wiki/Unit_in_the_last_place) сравниваемых чисел\.

Так как стандарт C\+\+ не форсирует IEEE 754, проверки на _x \=\= NaN_ через его свойство _\(x \!\= x\) \=\= true_ могут быть убраны компилятором как заведомо ложные\. Проверять нужно с помощью предназначенных для этого функций _std::isnan_\.

Поддерживается или нет IEEE 754, можно проверить с помощью предопределённой константы _std::numeric\_limits<FloatType\>::is\_iec559_

Сужающие преобразования из _float_ в знаковые или беззнаковые целые могут повлечь неопределённое поведение, если значение непредставимо в целочисленном типе\. Никаких обрезок по модулю _2^N_ не предполагается\.

```cpp
constexpr uint16_t x = 1234567.0; // CE, undefined behavior
```

Обратное преобразование \(из целочисленных типов во _float_/_double_\) также имеет свои подвохи, не связанные с неопределённым поведением: большие по абсолютной величине целые числа [теряют точность](https://godbolt.org/z/xnr5rMGKf)\.

```cpp
static_assert(
  static_cast<float>(std::numeric_limits<int>::max()) ==  // OK
  static_cast<float>(static_cast<long long>(
     std::numeric_limits<int>::max()) + 1) 
);

static_assert(
 static_cast<double>((1LL << 53) - 1) == static_cast<double>(1LL << 53) // Fire!
);

static_assert(
 static_cast<double>((1LL << 54) - 1) == static_cast<double>(1LL << 54) // OK
);

static_assert(
 static_cast<double>((1LL << 55) - 1) == static_cast<double>(1LL << 55) // OK
);

static_assert(
 static_cast<double>((1LL << 56) - 1) == static_cast<double>(1LL << 56) // OK
);
```

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

### Плавающая точка и шаблоны

До C\+\+20 вещественные числа нельзя было использовать в качестве параметров\-значений в шаблонах\. Теперь же можно\. Правда, ожидать, что вы насчитаете в run\-time и в compile\-time одно и то же, [не стоит](https://godbolt.org/z/WeGfbaqj8)\.

<details>
   <summary>Отличие run\\\-time и в compile\\\-time\\\.</summary>

Код, правда, на C, но суть та же\. Первый вызов функции [_expl_](https://en.cppreference.com/w/c/numeric/math/exp) \(возведение числа _E_ в степень _X_\) разворачивается в константу, а второй по\-честному вычисляется:

```cpp
#include <stdio.h>
#include <string.h>
#include <math.h>

static void printBits(size_t const size, void const * const ptr)
{
    unsigned char *b = (unsigned char*) ptr;
    unsigned char byte;
    int i, j;

    for (i = size * 8; i > 0; i--) {
        if( i % 8 == 0)
        {
            printf("%d", i);
            if( i >= 100) i-=2;
            else if( i >= 10) i-=1;
        }
        else {printf(" ");}
    }
    printf("\n");
    for (i = size * 8; i > 0; i--) {
        if( i%8 == 0) {printf("|");} else {printf(" ");}
    }
    printf("\n");
    for (i = size-1; i >= 0; i--) {
        for (j = 7; j >= 0; j--) {
            byte = (b[i] >> j) & 1;
            printf("%u", byte);
        }
    }
    printf("\n");
}

int main()
{
    long double c, r1, r2;

    r1 = expl(-1);

    c = -1;
    r2 = expl(c);

    printBits(sizeof(r1), &r1);
    printBits(sizeof(r2), &r2);

    if( memcmp( &r1, &r2, sizeof(r1)) != 0 )
    {
        printf("Not equal!\n");
        return 1;
    }

    printf("Equal!\n");
    return 0;
}
```

Давайте посмотрим на результат работы кода\. Различаются не только run\-time и compile\-time варианты вычислений\. На результат влияют ещё и ключи оптимизации\.

Вывод при использовании компилятора _x86\-64 GCC 14\.1_:

![1136_book_pt_2_ru/image3.png](https://import.viva64.com/docx/blog/1136_book_pt_2_ru/image3.png)



Вывод при использовании компилятор _x86\-64 GCC 14\.1_ с ключом **\-O3**:

![1136_book_pt_2_ru/image4.png](https://import.viva64.com/docx/blog/1136_book_pt_2_ru/image4.png)


</details>


Для простой параметризации типов константами этот механизм вполне можно использовать без опасений\. Однако строить на них паттерн\-матчинг с выбором специализаций шаблонов крайне [не рекомендуется](https://godbolt.org/z/cGf9h94cn):

```cpp
template <double x>
struct X {
    static constexpr double val = x;
};

template <>
struct X<+0.> {
    static constexpr double val = 1.0;
};

template <>
struct X<-0.> {
    static constexpr double val = -1.0;
};


int main() {
    constexpr double a = -3.0;
    constexpr double b = 3.0;
    std::cout << X<a + b>::val << "\n";          // печатает +1
    std::cout << X<-1.0 * (a + b)>::val << "\n"; // печатает -1
    static_assert(a + b == -1.0 * (a + b));      // ok
}
```

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

#### Полезные ссылки

1. Cppreference\. [std::isnan](https://en.cppreference.com/w/cpp/numeric/math/isnan)\.
1. Matt Kline\. [Comparing Floating\-Point Numbers Is Tricky](https://bitbashing.io/comparing-floats.html)\.
1. Diego Assencio\. [Avoid using floating\-point numbers as hash table keys](https://dassencio.org/98)\.
1. Андрей Карпов\. [60 антипаттернов для С\+\+ программиста](https://pvs-studio.ru/ru/blog/posts/cpp/1053/) \(вредный совет N14: double \=\= double\)\.

## Целые и вещественные числа: Integer promotion

C\+\+ от C досталось тяжёлое наследство\. Одна его часть была исправлена и беспощадно зарезана для большей надёжности: так, например, поступили с неявными _const_ преобразованиями\. Другая же часть, доставляющая не меньше проблем, перешла в первозданном виде\.

В C и C\+\+ много различных типов целых чисел разных размеров\. И над ними определены операции\. Правда, операции определены не для каждого типа чисел\.

Например, здесь нет \+, \-, \*, / для _uint16\_t_\. Но применить мы их можем, и [результатом](https://godbolt.org/z/bdKjsor9T) операций над беззнаковыми числами станет число со знаком\.

```cpp
uint16_t x = 1;
uint16_t y = 2;
auto a = x - y;   // а имеет тип int
auto b = x + y;   // b имеет тип int
auto c = x * y;   // c имеет тип int
auto d = x / y;   // d имеет тип int
```

Хотя это опять не вся правда\. Если _int_ окажется 16\-битным, то _a_, _b_, _c_ и _d_ станут _unsigned int_\. Ну и стоит тип хотя бы одного аргумента поменять на _uint32\_t_, как результат сразу же [теряет знак](https://godbolt.org/z/aY1nhdr37)\.

### Что происходит?

Происходят две неявные операции:

1. Типы, меньшие _int_, приводятся к _int_ \(integer promotion\)\. Знаковому\! Независимо от знаковости исходного типа\!
1. Когда в операции участвуют аргументы разных типов целых чисел, они приводятся к общему типу \(usual arithmetic conversion\):
    * Меньший тип приводится к большему;
    * Если размеры одинаковы, то знаковый приводится к беззнаковому\.

Аналогичные операции проводятся и над числами с плавающей точкой\. За полной таблицей и цепочкой, показывающей, что и в кого неявно превращается, стоит обратиться к тексту [стандарта](https://eel.is/c++draft/conv.rank)\.

### К чему это приводит?

1\. К ошибкам в логике\. Неявные преобразования вовлекаются в любую операцию\. Вы выполняете сравнение знакового и беззнакового числа и забыли явно привести типы? Готовьтесь к тому, что _\-1 < 1_ может [вернуть](https://godbolt.org/z/sqvrasjE4) _false_:

```cpp
std::vector<int> v = {1};
auto idx = -1;
if (idx < v.size()) {
    std::cout << "less!\n";
} else {
    std::cout << "oops!\n";
}
```

2\. К [неопределённому поведению](https://godbolt.org/z/M3Kx3e3q6):

```cpp
unsigned short x=0xFFFF;
unsigned short y=0xFFFF;
auto z=x*y;
```

Integer promotion неявно приводит _x_ и _y_ к _int_, в котором происходит переполнение\. Переполнение _int_ — неопределённое поведение\.

3\. К трудностям в переносе программ с одной платформы на другую\. Если меняется размер _int_/_long_, то применение правил неявных конверсий к вашему коду также [меняется](https://godbolt.org/z/hs59o3zca):

```cpp
std::cout << (-1L < 1U);
```

Код выводит разные значения в зависимости от размера типа _long_\.

### Что делать?

1. Не смешивать в одном выражении знаковые и беззнаковые типы\.
1. Уделять особое внимание коду, работающему с типами, меньшими _int_\.
1. Включать предупреждения от компилятора \(_\-Wconversion_, не всегда работает\)\.
1. Посматривать на диагностические сообщения анализаторов кода\.

#### Полезные ссылки

1. C\+\+ draft\. [Integral promotions](https://eel.is/c++draft/conv.prom)\.
1. C\+\+ draft\. [Usual arithmetic conversions](https://eel.is/c++draft/expr.arith.conv)\.
1. Stack Overflow\. [Implicit type promotion rules](https://stackoverflow.com/questions/46073295/implicit-type-promotion-rules)\.
1. Shafik Yaghmour\. [The Usual Arithmetic Confusions](https://shafik.github.io/c++/2021/12/30/usual_arithmetic_confusions.html)\.

## Целые и вещественные числа: char и знаковое расширение

Возьмём следующую простенькую структуру:

```cpp
// Пример взят и изменен отсюда:
// https://twitter.com/hankadusikova/status/1626960604412928002
struct CharTable {
    static_assert(CHAR_BIT == 8);
    std::array<bool, 256> _is_whitespace {};

    CharTable() {
        _is_whitespace.fill(false);
    }

    bool is_whitespace(char c) const {
        return this->_is_whitespace[c];
    }
};
```

Всё ли в порядке с этим безобидным методом _is\_whitespace_? Ну кроме того, что _char_ в C и C\+\+ обычно восьмибитный, а в Unicode [есть](https://jkorpela.fi/chars/spaces.html) пробельные символы, кодируемые 16 битами\.

Давайте [потестируем](https://godbolt.org/z/75rTW1nMG):

```cpp
int main() {
    CharTable table;
    char c = 128;
    bool is_whitespace = table.is_whitespace(c);
    std::cout << is_whitespace << "\n";
    return is_whitespace;
}
```

При сборке с _\-fsanitize\=undefined_ получаем дивный результат:

```cpp
/opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/array:61:36:
runtime error: index 18446744073709551488 out of bounds for type 'bool [256]'
/opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/array:61:36:
runtime error: index 18446744073709551488 out of bounds for type 'bool [256]'
/app/example.cpp:14:38:
runtime error: load of value 64, which is not a valid value for type 'bool'
```

Конкретное значение в третьей строке совершенно случайное\. Было бы очень здорово стабильно видеть 42, но увы\.

Зато индекс в первых двух строках совсем не случайный\.

Но погодите, _char c \= 128_, а это же точно меньше 256\. Откуда _18446744073709551488_?

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

1. Специфичная ловушка C и C\+\+: знаковость типа _char_ не специфицирована\. В зависимости от платформы он может быть как знаковым, так и беззнаковым\. На x86 чаще всего является знаковым\. И из _char c \= 128_ получается _c \= \-128_\.
1. Ловушка, распространённая во многих языках, имеющих разные типы целых чисел разной знаковости и длины\. Например, [Rust](https://godbolt.org/z/cY1v3rvrK):

```cpp
pub fn main() {
    let c : i8 = -5;
    let c_direct_cast = c as u16;
    let c_two_casts = c as u8 as u16;
    println!("{c_direct_cast} != {c_two_casts}");
}
```

Мы увидим _65531 \!\= 251_\.

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

То же [действует и в C и C\+\+](https://godbolt.org/z/cfcdb5fr3):

```cpp
int main() {
    int8_t c  = -5;
    uint16_t c_direct_cast = c;
    uint16_t c_two_casts = static_cast<uint8_t>(c);
    std::cout << c_direct_cast << " != " << c_two_casts;
}
```

Напечатает: _65531 \!\= 251_\.

А теперь остаётся только взглянуть на сигнатуру _std::array::operator\[\]_:

```cpp
reference operator[]( size_type pos );
```

_size\_type_ — это беззнаковый _size\_t_\. Под x86 он определённо больше, чем _char_\. Происходит прямой каст знакового _char_ в _size\_t_, знак расширяется, код ломается\. Дело закрыто\.

### Что делать?

Со знаковым расширением иногда способны помочь статические анализаторы\. Нужно понимать, что вы делаете при касте чисел и что хотите получить\. Часто можно встретить конструкцию вида _uint32\_t extended\_val \= static\_cast<uint32\_t\>\(byte\_val\) & 0xFF_, чтобы гарантированно занулить верхние байты и избежать знакового расширения\. Аналогичная конструкция может быть и при преобразовании _int32 \-\> uint64_, и при любых других комбинациях\. Только константу правильную писать не забывайте\.

Из\-за своей знаковой неспецифицированности тип _char_ очень опасен при работе с ним как с типом чисел\. Крайне рекомендуется пользоваться соответствующими типами _uint8\_t_ или _int8\_t_\. Или другими подходящими, если на вашей целевой платформе в _char_ внезапно не 8 бит\.

#### Полезные ссылки

1. Cppreference\. [Fundamental types](https://en.cppreference.com/w/cpp/language/types)\. 
1. Cppreference\. [std::array<T,N\>::operator\[\]](https://en.cppreference.com/w/cpp/container/array/operator_at)\.
1. Cppreference\. [C numeric limits interface](https://en.cppreference.com/w/cpp/types/climits)\.
1. Sun Studio 12: C User's Guide\. [Sign Extension](https://docs.oracle.com/cd/E19205-01/819-5265/bjamz/index.html)\. 

**Автор — Дмитрий Свиридкин**

Более восьми лет работает в сфере коммерческой разработки высокопроизводительного программного обеспечения на C и C\+\+\. С 2019 по 2021 год преподавал курсы системного программирования под Linux в СПбГУ и практики C\+\+ в ВШЭ\.  В настоящее время — Software Engineer в AWS \(Cloudfront\), занимается системной и embedded\-разработкой на Rust и C\+\+ для edge\-серверов\. Основная сфера интересов — безопасность программного обеспечения\. 

**Редактор — Андрей Карпов**

Более 15 лет занимается темой статического анализа кода и качества программного обеспечения\. Автор большого количества статей, посвящённых написанию качественного кода на языке C\+\+\. С 2011 по 2021 год удостаивался награды Microsoft MVP в номинации Developer Technologies\. Один из основателей проекта PVS\-Studio\. Долгое время являлся CTO компании и занимался разработкой С\+\+ ядра анализатора\. Основная деятельность на данный момент — управление командами, обучение сотрудников и DevRel активности\.

## Все части

1. [Часть 1](https://pvs-studio.ru/ru/blog/posts/cpp/1129/): предисловие, что такое неопределённое поведение и как оно проявляется, сужающие преобразования и неявное приведение типов\.
1. [Часть 2](https://pvs-studio.ru/ru/blog/posts/cpp/1136/): переполнение целых знаковых чисел, числа с плавающей точкой, integer promotion, _char_ и знаковое расширение\.
1. [Часть 3](https://pvs-studio.ru/ru/blog/posts/cpp/1149/): висячие ссылки, _string\_view_, синтаксический сахар с ложкой дёгтя \(range\-based for\), self\-reference, _std::vector_ и инвалидация ссылок\.
1. [Часть 4](https://pvs-studio.ru/ru/blog/posts/cpp/1156/): списки захвата лямбда\-функций, кортежи, внезапная мутабельность, неявные ссылки, use\-after\-move, lifetime extension\.
1. [Часть 5](https://pvs-studio.ru/ru/blog/posts/cpp/1160/): Most Vexing Parse, неконстантные константы, семантика перемещения, _std::enable\_if\_t_ против _std::void\_t_, забытый _return_\.
1. [Часть 6](https://pvs-studio.ru/ru/blog/posts/cpp/1163/): эллипсис и функции, _operator \[\]_, _iostreams_ \(счастливой отладки\!\), оператор запятая, function\-try\-block, типы "нулевого" размера\.
1. [Часть 7](https://pvs-studio.ru/ru/blog/posts/cpp/1174/): NULL\-терминированные строки, _std::shared\_ptr_, \(не\)явное приведение типов, как передать стандартную функцию и ничего не сломать\.
1. [Часть 8](https://pvs-studio.ru/ru/blog/posts/cpp/1178/): бесконечные циклы и проблема остановки, рекурсия, ложный _noexcept_, переполнение буфера\.
1. [Часть 9](https://pvs-studio.ru/ru/blog/posts/cpp/1182/): \(N\)RVO vs RAII, разыменование нулевых указателей, static initialization order fiasco, static inline, нарушение ODR, зарезервированные имена\.
1. [Часть 10](https://pvs-studio.ru/ru/blog/posts/cpp/1193/): тривиальные типы и ABI, неинициализированные переменные, С\+\+20 unbounded ranges, невиртуальные виртуальные функции, VLA\.
1. [Часть 11](https://pvs-studio.ru/ru/blog/posts/cpp/1199/): невалидные указатели, placement new для массивов, data race, повторный захват mutex, сигнало\(не\)безопасность, как сделать всё правильно и уйти в deadlock\.
1. [Часть 12](https://pvs-studio.ru/ru/blog/posts/cpp/1211/): _std::vector::reserve_ и _std::vector::resize_, невыровненные ссылки, время жизни и смерти, статический анализ и UB, заключение\.