V108. Incorrect index type: 'foo[not a memsize-type]'. Use memsize type instead

15.12.2011

Анализатор обнаружил потенциально возможную ошибку индексации больших массивов. Ошибка может заключаться в некорректном вычислении индекса.

Первый пример.

extern char *longString;
extern bool *isAlnum;
...
unsigned i = 0;
while (*longString) {
  isAlnum[i] = isalnum(*longString++);
  ++i;
}

Данный код совершенно корректен для 32-битной платформы, где принципиально невозможна обработка массивов более UINT_MAX байт (4 Гб). На 64-битной платформе можно обработать массив размером более 4 Гб, что иногда очень удобно. Но корректно обработать большой массив приведенным алгоритмом невозможно. Ошибка заключается в использовании для индексации массива isAlnum переменной типа unsigned. Когда мы заполним первые UINT_MAX элементов, произойдет переполнение переменной i, и она приравняется нулю. В результате мы начнем перезаписывать элементы массива isAlnum расположенные в начале, а часть элементов оставим неинициализированными.

Корректным исправлением является изменения типа переменной i на memsize тип:

...
size_t  i = 0;
while (*longString)
  isAlnum[i++] = isalnum(*longString++);

Второй пример.

class Region {
  float *array;
  int Width, Height, Depth;
  float Region::GetCell(int x, int y, int z) const;
  ...
};
float Region::GetCell(int x, int y, int z) const {
  return array[x + y * Width + z * Width * Height];
}

Для программ численного моделирования важным ресурсом является объем оперативной памяти, и возможность на 64-битной архитектуре использовать более 4 Гб памяти существенно увеличивает вычислительные возможности. В таких программах часто используют одномерные массивы, работая затем с ними как с трехмерными. Для этого существуют функции, аналогичные GetCell, обеспечивающие доступ к необходимым элементам счетной области. Но приведенный код может корректно работать с массивами, содержащими не более INT_MAX (2 Гб) элементов. Причина заключается в использовании 32-битных типов int, участвующих в вычислении индекса элемента. Если количество элементов в массиве array превысит INT_MAX (2 Гб), то произойдет переполнение и значение индекса будет вычислено некорректно. Программисты часто допускают ошибку, пытаясь исправить код следующим образом:

float Region::GetCell(int x, int y, int z) const {
  return array[static_cast<ptrdiff_t>(x) + y * Width +
                    z * Width * Height];
}

Они знают, что по правилам языка Си++ выражение для вычисления индекса будет иметь тип ptrdiff_t и надеются за счет этого избежать переполнения. К сожалению, переполнение может произойти внутри подвыражения y * Width или z * Width * Height,так как для их вычисления по-прежнему используется тип int.

Если вы хотите исправить код, не изменяя типов переменных, участвующих в выражении, то вы должны явно привести каждую переменную к memsize типу:

float Region::GetCell(int x, int y, int z) const {
  return array[ptrdiff_t(x) +
 ptrdiff_t(y) * ptrdiff_t(Width) +
 ptrdiff_t(z) * ptrdiff_t(Width) *
 ptrdiff_t(Height)];
}

Другое решение - изменить типы переменных на memsize тип:

class Region {
  float *array;
  ptrdiff_t Width, Height, Depth;
  float
    Region::GetCell(ptrdiff_t x, ptrdiff_t y, ptrdiff_t z) const;
  ...
};
float Region::GetCell(ptrdiff_t x, ptrdiff_t y, ptrdiff_t z) const
{
  return array[x + y * Width + z * Width * Height];
}

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

bool *Seconds;
int min, sec;
...
bool flag = Seconds[static_cast<size_t>(min * 60 + sec)];

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

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

char Arr[] = { '0', '1', '2', '3', '4' };
char *p = Arr + 2;
cout << p[0u + 1] << endl;
cout << p[0u - 1] << endl; //V108

Данный код исправно работает в 32-битном режиме и печатает на экране числа 3 и 1. При проверке этого кода мы получим предупреждение только на одну строку с выражением "p[0u - 1]". И это совершенно верно. Если вы скомпилируете и запустите данный пример в 64-битном режиме, то увидите, как на экране будет распечатано значение 3, после чего произойдет аварийное завершений программы.

Ошибка связана с тем, что индексация "p[0u - 1]" на 64-битной системе некорректна, о чем и предупреждает анализатор. Согласно правилам языка Си++ выражение "0u - 1" будет иметь тип unsigned и равняться 0xFFFFFFFFu. На 32-битной архитектуре сложение указателя с этим числом будет эквивалентно вычитанию единицы. А на 64-битной системе к указателю будет честно прибавлено 0xFFFFFFFFu и произойдет обращение к памяти за приделами массива.

Конечно, часто индексация к массивам с использованием таких типов как int и unsigned безопасна. В этом случае предупреждения анализатора могут казаться не уместными. Но следует учитывать, что такой код все равно может оказаться ненадежным в случае его модернизации для обработки другого набора данных. Код с использованием типов int и unsigned может оказаться менее производительным, чем это возможно на 64-битной архитектуре.

Если вы уверены в корректности индексации, то вы можете воспользоваться функцией "Suppression of false alarms" или использовать фильтры. Можно использовать явное приведение типов в коде:

for (int i = 0; i != n; ++i)
  Array[static_cast<ptrdiff_t>(i)] = 0;