>
>
>
Поиск 64-битных ошибок в реализации мас…

Андрей Карпов
Статей: 674

Поиск 64-битных ошибок в реализации массивов

В PVS-Studio 3.43 был пересмотрен подход в обнаружении анализатором Viva64 ошибок в классах, представляющих собой контейнеры (массивы). Ранее мы придерживались позиции, что если в классе реализован operator[], то его параметр должен иметь memsize-тип (ptrdiff_t, size_t), а не int или unsigned. Мы и сейчас рекомендуем использовать для operator[] в качестве аргумента memsize тип. Это позволяет компилятору построить в ряде случаев более эффективный код и заранее предотвращает некоторые 64-битные ошибки. Сейчас мы изменили подход к работе с классами, имеющими operator[], что позволяет сократить количество лишних диагностических предупреждений.

Рассмотрим пример, который потенциально может содержать ошибку, если мы захотим работать с большими объемами данных:

class MyArray {
  std::vector <float> m_arr;
  ...
  float &operator[](int i)
  {
    return m_arr[i];
  }
} A;
...
int x = 2000;
int y = 2000;
int z = 2000;
A[x * y * z] = 33;

Первый недостаток кода заключается в том, что operator[] не позволяет осуществить доступ к элементу с номером более INT_MAX.

Примечание. Хочу уточнить один важный момент. Для подобного кода, что показан в примере, компилятор в release-версии может провести такую оптимизацию, что будет работать, так как будет использоваться 64-битных регистр для вычисления и передачи индекса. Я посвящу отдельный пост более подробному рассмотрению этого примера. Однако это везение не делает код корректным. Подробнее про опасные оптимизации смотрите здесь.

Второй недостаток кода заключается в выражении x*y*z, в котором может возникнуть переполнение при работе с большим массивом.

Ранее анализатор выдавал два предупреждения (V108). Первое - использование типа int при обращении к массиву m_arr. Второе - использование типа int при обращении к массиву A. Хотя operator[] класса MyArray принимает аргумент int, мы предлагали использовать в качестве индекса memsize-тип. Когда программист исправлял тип переменных x, y и z на ptrdiff_t компилятор Visual C++ начинал предупреждать о приведении типа в строке A[x * y * z] = 33:

warning C4244: 'argument' : conversion from 'ptrdiff_t' to 'int', possible loss of data

Это предупреждение подсказывало пользователю изменить аргумент в operator[] и код становился полностью корректным. Пример исправленного кода:

class MyArray {
  std::vector <float> m_arr;
  ...
  float &operator[](ptrdiff_t i)
  {
    return m_arr[i];
  }
} A;
...
ptrdiff_t x = 2000;
ptrdiff_t y = 2000;
ptrdiff_t z = 2000;
A[x * y * z] = 33;

К сожалению, у данного подхода диагностики выяснился существенный недостаток. В ряде случаев operator[] недоступен для изменения, или использование int в качестве индекса полностью оправдано. При этом получалось, что анализатор Viva64 генерирует множество лишних предупреждений. Примером может служить использование класса CString из библиотеки MFC. Оператор в классе CString имеет прототип:

TCHAR operator []( int nIndex ) const;

Из-за этого данный код диагностировался как опасный:

int i = x;
CString s = y;
TCHAR c = s[i];

Класс CString недоступен для правки. Да и вряд ли кто будет в стандартной программе использовать тип CString для работы со строками длиннее 2-х миллиардов символов. В свою очередь анализатор Viva64 выдавал множество предупреждений на данный код. Если программист менял тип индекса с int на ptrdiff_t, то предупреждения начинал выдавать компилятор. Можно было использовать подавление предупреждений //-V108, но это загромождает код. Подробнее подавление предупреждений можно изучить в статье: PVS-Studio: использование функции "Mark as False Alarm".

Было принято решение считать конструкцию A[x * y * z] = 33; из первого примера безопасной. Теперь если operator[] в качестве аргумента принимает 32-битный тип (например, int), и мы вызываем этот оператор так же используя 32-битный тип, то данный вызов считается безопасным.

Естественно это может замаскировать ошибку. Поэтому было добавлено новое диагностическое сообщение V302: "Member operator[] of 'FOO' class has a 32-bit type argument. Use memsize-type here". Это диагностическое сообщение выводится для operator[], объявленных с 32-битным аргументом.

Изящность этого решения заключается в том, что для библиотечного кода, к которому нет доступа для изменений, данное сообщение выводиться не будет. То есть предупреждение V302 не будет выдано для класса CString, но будет выдано для пользовательского класса MyArray.

Если operator[] в классе MyArray корректен и действительно должен иметь тип int, то программисту будет достаточно вписать только одно подавление предупреждения //-V302 в данном классе, а не во множестве мест, где он будет использоваться.

Последнее изменение, связанное с обработкой массивов, касается введения еще одного предупреждения V120: "Member operator[] of object 'FOO' declared with 32-bit type argument, but called with memsize type argument". Это предупреждение в целом дублирует предупреждение компилятора о приведении 64-битного типа к 32-битному. Оно будет полезно в том случае, когда предупреждений от компилятора много и в них теряется информация, связанная с работоспособностью кода на 64-битной системе.