﻿# Урок 17\. Паттерн 9\. Смешанная арифметика

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

Смешанное использование [memsize](https://pvs-studio.ru/ru/blog/terms/0030/)\- и не memsize\-типов в выражениях может приводить к некорректным результатам на 64\-битных системах и быть связано с изменением диапазона входных значений\. Рассмотрим ряд примеров:

```cpp
size_t Count = BigValue;
for (unsigned Index = 0; Index != Count; ++Index)
{ ... }
```

Это пример вечного цикла, если Count \> UINT\_MAX\. Предположим, что на 32\-битных системах этот код работал с количеством итераций менее значения UINT\_MAX\. Но 64\-битный вариант программы может обрабатывать больше данных, и ему может потребоваться большее количество итераций\. Поскольку значения переменной Index лежат в диапазоне \[0\.\.UINT\_MAX\], то условие "Index \!\= Count" никогда не выполнится, что и приводит к бесконечному циклу\.

_Примечание\. Заметим, что при определенных настройках компилятора данный пример может успешно отработать\. Из\-за этого иногда, кажется, что код корректен, и возникает путаница\. В одном из последующих уроков мы расскажем о фантомных ошибках, проявляющих себя только временами\. Если вам интересно уже сейчас понять это странное поведение кода, то предлагаем ознакомиться со статьей "[64\-битный конь, который умеет считать](https://pvs-studio.ru/ru/blog/posts/cpp/a0043/)"\._

Для исправления кода необходимо использовать в выражениях только memsize\-типы\. В данном примере можно заменить тип переменной Index с unsigned на [size\_t](https://pvs-studio.ru/ru/blog/terms/0044/)\.

Другая часто встречающаяся ошибка \- запись выражений следующего вида:

```cpp
int x, y, z;
ptrdiff_t SizeValue = x * y * z;
```

Ранее уже рассматривались подобные примеры, когда при вычислении значений с использованием не memsize\-типов происходило арифметическое переполнение\. И конечный результат был некорректен\. Поиск и исправление приведенного кода осложняется тем, что компиляторы, как правило, не выдают на него никаких предупреждений\. С точки зрения языка Си\+\+ это совершенно корректная конструкция\. Происходит умножение нескольких переменных типа int, после чего результат неявно расширяется до типа [ptrdiff\_t](https://pvs-studio.ru/ru/blog/terms/0041/) и происходит присваивание\.

Приведем небольшой код, показывающий опасность неаккуратных выражений со смешанными типами \(результаты получены с использованием Microsoft Visual C\+\+ 2005, 64\-битный режим компиляции\):

```cpp
int x = 100000;
int y = 100000;
int z = 100000;
ptrdiff_t size = 1;                   // Result:
ptrdiff_t v1 = x * y * z;             // -1530494976
ptrdiff_t v2 = ptrdiff_t (x) * y * z; // 1000000000000000
ptrdiff_t v3 = x * y * ptrdiff_t (z); // 141006540800000
ptrdiff_t v4 = size * x * y * z;      // 1000000000000000
ptrdiff_t v5 = x * y * z * size;      // -1530494976
ptrdiff_t v6 = size * (x * y * z);    // -1530494976
ptrdiff_t v7 = size * (x * y) * z;    // 141006540800000
ptrdiff_t v8 = ((size * x) * y) * z;  // 1000000000000000
ptrdiff_t v9 = size * (x * (y * z));  // -1530494976
```

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

```cpp
ptrdiff_t v2 = ptrdiff_t (x) + y * z;
```

вовсе не гарантирует правильный результат\. Оно гарантирует только то, что выражение " ptrdiff\_t \(x\) \+ y \* z" будет иметь тип ptrdiff\_t\.

Следовательно, если результатом выражения должен являться memsize\-тип, то в выражении должны участвовать только memsize\-типы\. Или элементы, приведенные к memsize\-типам\. Правильный вариант:

```cpp
ptrdiff_t v2 = ptrdiff_t (x) + ptrdiff_t (y) * ptrdiff_t (z); // OK!
```

Впрочем не всегда необходимо приводить все аргументы к memsize\-типу\. Если выражение состоит из одинаковых операторов, то достаточно привести к memsize\-типу только первый аргумент\. Рассмотрим пример:

```cpp
int c();
int d();
int a, b;
ptrdiff_t v2 = ptrdiff_t (a) * b * c() * d();
```

Порядок вычисления выражения с операторами одинакового приоритета не определен\. Точнее, компилятор волен вычислять подвыражения, например вызов функций c\(\) и d\(\) в том порядке, который он считает более эффективным, даже если подвыражения вызывают побочные эффекты\. Порядок возникновения побочных эффектов не определен\. Но поскольку операция умножения относится к лево\-ассоциативным операторам, то вычисление будет происходить следующим образом:

```cpp
ptrdiff_t v2 = ((ptrdiff_t (a) * b) * c()) * d();
```

В результате каждый из операндов перед умножением будет преобразовываться к типу ptrdiff\_t и мы получим корректный результат\.

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

Смешанное использование типов может проявляться и в изменении программной логики:

```cpp
ptrdiff_t val_1 = -1;
unsigned int val_2 = 1;
if (val_1 > val_2)
  printf ("val_1 is greater than val_2\n");
else
  printf ("val_1 is not greater than val_2\n");
//Output on 32-bit system: "val_1 is greater than val_2"
//Output on 64-bit system: "val_1 is not greater than val_2"
```

На 32\-битной системе переменная val\_1 согласно правилам языка Си\+\+ расширялась до типа unsigned int и становилась значением 0xFFFFFFFFu\. В результате условие "0xFFFFFFFFu \> 1" выполнялось\. На 64\-битной системе наоборот расширяется переменная val\_2 до типа ptrdiff\_t\. В этом случае уже проверяется выражение "\-1 \> 1"\. На рисунке 1 и 2 схематично отображены происходящие преобразования\.

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

_Рисунок 1 \- Преобразования, происходящие в 32\-битном коде_

![17_Pattern_09_Mixed_arithmetic_ru/image2.png](https://import.viva64.com/docx/blog/17_Pattern_09_Mixed_arithmetic_ru/image2.png)

_Рисунок 2 \- Преобразования, происходящие в 64\-битном коде_

Если вам необходимо вернуть прежнее поведение кода \- следует изменить тип переменной val\_2:

```cpp
ptrdiff_t val_1 = -1;
size_t val_2 = 1;
if (val_1 > val_2)
  printf ("val_1 is greater than val_2\n");
else
  printf ("val_1 is not greater than val_2\n");
```

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

Мы рассмотрели только простые выражения\. Но описываемые проблемы могут проявиться и при использовании других конструкций языка Си\+\+:

```cpp
extern int Width, Height, Depth;
size_t GetIndex(int x, int y, int z) {
  return x + y * Width + z * Width * Height;
}
...
MyArray[GetIndex(x, y, z)] = 0.0f;
```

В случае работы с большими массивами \(более INT\_MAX элементов\) данный код будет вести себя некорректно, и мы будем адресоваться не к тем элементам массива MyArray, к которым рассчитываем\. Несмотря на то, что мы возвращаем значение типа size\_t, выражение "x \+ y \* Width \+ z \* Width \* Height" вычисляется с использованием типа int\. Мы думаем, вы уже догадались, что исправленный код будет выглядеть следующим образом:

```cpp
extern int Width, Height, Depth;
size_t GetIndex(int x, int y, int z) {
  return (size_t)(x) +
         (size_t)(y) * (size_t)(Width) +
         (size_t)(z) * (size_t)(Width) * (size_t)(Height);
}
```

Или чуть более просто:

```cpp
extern int Width, Height, Depth;
size_t GetIndex(int x, int y, int z) {
  return (size_t)(x) +
         (size_t)(y) * Width +
         (size_t)(z) * Widt) * Height;
}
```

В следующем примере, у нас вновь смешивается memsize\-тип \(указатель\) и 32\-битный тип unsigned:

```cpp
extern char *begin, *end;
unsigned GetSize() {
  return end - begin;
}
```

Результат выражения "end \- begin" имеет тип ptrdiff\_t\. Поскольку функция возвращает тип unsigned, то происходит неявное приведение типа, при котором старшие биты результата теряются\. Таким образом, если указатели begin и end ссылаются на начало и конец массива, по размеру большего UINT\_MAX \(4Gb\), то функция вернет некорректное значение\.

И еще один пример\. На этот раз рассмотрим не возвращаемое значение, а формальный аргумент функции:

```cpp
void foo(ptrdiff_t delta);
int i = -2;
unsigned k = 1;
foo(i + k);
```

Этот код не напоминает вам пример с некорректной [арифметикой указателей](https://pvs-studio.ru/ru/blog/terms/0005/), рассмотренный в 13\-том уроке? Да, здесь происходит то же самое\. Некорректный результат возникает при неявном расширении фактического аргумента, имеющего значение 0xFFFFFFFF и тип unsigned, до типа ptrdiff\_t\.

## Диагностика

Ошибки, возникающие на 64\-битных системах при смешанном использование простых целочисленных типов и memsize\-типов, представлены большим количеством синтаксических конструкций языка Си\+\+\. Для диагностики этих ошибок используется целый ряд диагностических сообщений\. Анализатор PVS\-Studio предупреждает о потенциально возможных ошибках, используя следующие сообщения: [V101](https://pvs-studio.ru/ru/docs/warnings/v101/), [V103](https://pvs-studio.ru/ru/docs/warnings/v103/), [V104](https://pvs-studio.ru/ru/docs/warnings/v104/), [V105](https://pvs-studio.ru/ru/docs/warnings/v105/), [V106](https://pvs-studio.ru/ru/docs/warnings/v106/), [V107](https://pvs-studio.ru/ru/docs/warnings/v107/), [V109](https://pvs-studio.ru/ru/docs/warnings/v109/), [V110](https://pvs-studio.ru/ru/docs/warnings/v110/), [V121](https://pvs-studio.ru/ru/docs/warnings/v121/)\.

Вернемся к ранее рассмотренному примеру:

```cpp
int c();
int d();
int a, b;
ptrdiff_t x = ptrdiff_t(a) * b * c() * d();
```

Хотя само выражение перемножает аргументы, расширяя их тип до ptrdiff\_t, ошибка может содержаться в вычислении самих этих аргументов\. Поэтому анализатор все равно предупреждает о смешивании типов: "V104: Implicit type conversion to memsize type in an arithmetic expression"\.

Также инструмент PVS\-Studio позволяет найти потенциально опасные выражения, которые скрываются за явным приведением типов\. Для этого можно включить в настройках анализатора предупреждения [V201](https://pvs-studio.ru/ru/docs/warnings/v201/) и [V202](https://pvs-studio.ru/ru/docs/warnings/v202/)\. По умолчанию анализатор не выдает предупреждения связанные с приведением типа, в случае, когда приведение осуществляется явно\. Пример:

```cpp
TCHAR *begin, *end;
unsigned size = static_cast<unsigned>(end - begin);
```

Выявить подобный некорректный код и позволяют сообщения V201 и V202\.

При этом анализатор не обратит внимания на безопасные с точки зрения 64\-битного кода приведения типов:

```cpp
const int *constPtr;
int *ptr = const_cast<int>(constPtr);
float f = float(constPtr[0]);
char ch = static_cast<char>(sizeof(double));
```

_Авторы курса: Андрей Карпов \([karpov@viva64\.com](mailto:karpov@viva64.com)\), Евгений Рыжков \([evg@viva64\.com](mailto:evg@viva64.com)\)\._

_Правообладателем курса "Уроки разработки 64\-битных приложений на языке Си/Си\+\+" является ООО "Системы программной верификации"\. Компания занимается разработкой программного обеспечения в области анализа исходного кода программ\. [Сайт компании](https://pvs-studio.ru/ru/pvs-studio/)\._