Анализатор обнаружил арифметическое выражение, в котором может произойти переполнение знакового числа.
Рассмотрим пример:
long long foo() { long longOperand = 0x7FFF'FFFF; long long y = longOperand * 0xFFFF; return y; }
По правилам C и C++ результирующим типом выражения 'longOperand * 0xFFFF' будет 'long'. При использовании компилятора MSVC на Windows тип 'long' имеет размер 4 байта. Максимальное значение, которое может быть представлено этим типом, равно 2'147'483'647 в десятичной системе счисления или 0x7FFF'FFFF в шестнадцатеричной. При умножении переменной 'longOperand' на 0xFFFF (65 535) ожидается результат 0x7FFF'7FFF'0001. Однако согласно стандарту C (см. стандарт С18 пункт 6.5 параграф 5) и C++ (см. стандарт С++20 пункт 7.1 параграф 4) переполнение знаковых чисел приводит к неопределённому поведению.
Исправить этот код можно несколькими способами в зависимости от того, чего хочет программист.
Если требуется произвести корректные вычисления, необходимо использовать типы, размеры которых будут достаточны для отображения чисел. Если число не помещается в машинное слово, то можно воспользоваться одной из библиотек по длинной арифметике. Например, GMP, MPRF, cnl.
Пример выше можно исправить следующим образом:
long long foo() { long longOperand = 0x7FFF'FFFF; long long y = static_cast<long long>(longOperand) * 0xFFFF; return y; }
Если переполнение знаковых чисел – это неожидаемое поведение, и его требуется обработать каким-либо образом, то можно воспользоваться специальными библиотеками для безопасной работы с числами. Например, boost::safe_numerics или Google Integers.
Если требуется реализовать циклическую арифметику для знаковых чисел с определённым по стандарту поведением, то для расчётов можно воспользоваться беззнаковыми числами. В случае их переполнения происходит "оборачивание" числа по модулю '2 ^ n', где n – количество бит в числе.
Рассмотрим одно из возможных решений на основе 'std::bit_cast' (C++20):
#include <concepts> #include <type_traits> #include <bit> #include <functional> namespace detail { template <std::signed_integral R, std::signed_integral T1, std::signed_integral T2, std::invocable<std::make_unsigned_t<T1>, std::make_unsigned_t<T2>> Fn> R safe_signed_wrapper(T1 lhs, T2 rhs, Fn &&op) noexcept(std::is_nothrow_invocable_v<Fn, std::make_unsigned_t<T1>, std::make_unsigned_t<T2>>) { auto uLhs = std::bit_cast<std::make_unsigned_t<T1>>(lhs); auto uRhs = std::bit_cast<std::make_unsigned_t<T2>>(rhs); auto res = std::invoke(std::forward<Fn>(op), uLhs, uRhs); using UR = std::make_unsigned_t<R>; return std::bit_cast<R>(static_cast<UR>(res)); } }
Функция 'std::bit_cast' приводит 'lhs' и 'rhs' к соответствующим беззнаковым представлениям. Далее на двух преобразованных операндах выполняется некоторая арифметическая операция. Затем результат расширяется или сужается до нужного результирующего типа и превращается в знаковый.
При таком подходе знаковые числа будут повторять семантику беззнаковых в арифметических операциях, что в свою очередь не будет приводить к неопределённому поведению.
Например, по этой ссылке можно убедиться, что компилятор имеет право оптимизировать код, если видит, что может произойти переполнение знакового числа. Рассмотрим его подробнее:
bool is_max_int(int32_t a) { return a + 1 < a; }
Если 'a' равно 'MAX_INT', то условие 'a + 1 < a' будет равно 'false'. Таким образом часто проверяют не произошло ли переполнение. Однако компилятор генерирует такой код:
is_max_int(int): # @is_max_int(int) xor eax, eax ret
Инструкция ассемблера 'xor eax, eax' обнуляет результат выполнения функции 'is_max_int'. В результате последняя всегда возвращает 'true' вне зависимости от значения 'a'. В данном случае это результат неопределённого поведения при переполнении.
В случае применения беззнакового представления такого не происходит:
is_max_int(int): # @is_max_int(int) cmp edi, 2147483647 sete al ret
Компилятор сгенерировал код, который честно проверяет условие.
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V1083. |