Ловушка для невнимательных: почему unsigned int — это бомба замедленного действия?
Каждый C++ разработчик рано или поздно сталкивается с ситуацией, когда числа ведут себя как злые кролики — бесконечно прыгают из плюса в ноль. Речь идет о переполнении unsigned int. В отличие от своего подписанного собрата (signed int), переполнение unsigned в C++ строго определено стандартом — это не UB (undefined behavior). Число просто оборачивается по модулю 2N. Но это не делает ошибку менее фатальной.
Представьте: вы считаете баланс на счете, количество элементов в массиве или время работы таймера. Одно случайное переполнение — и логика приложения рушится. Вопрос из заголовка "How do I detect unsigned integer overflow?" собрал тысячи просмотров на Stack Overflow не просто так. Давайте разберем три подхода к детекции переполнения: от наивного (но работающего) до профессионального.
Почему стандартные проверки не работают?
Первое, что приходит в голову новичку:
unsigned int a = UINT_MAX;
unsigned int b = 1;
if (a + b < a) // Наивная проверка
std::cout << "Переполнение!";
Этот код работает, но только для сложения. Для умножения, вычитания или смешанных типов логика ломается. Кроме того, компилятор может посчитать такую проверку избыточной и выкинуть её при оптимизации. Особенно если типы знаковые. Не надейтесь на магию — пишите явно.
Подход №1: Арифметика до операции (Pre-check)
Самый надежный способ — проверить возможность переполнения до выполнения операции. Для сложения двух беззнаковых чисел условие простое:
Если a + b может переполниться, то a > (UINT_MAX - b).
#include <climits>
#include <iostream>
bool will_add_overflow(unsigned int a, unsigned int b) {
return a > UINT_MAX - b;
}
int main() {
unsigned int x = 4000000000;
unsigned int y = 500000000;
if (will_add_overflow(x, y)) {
std::cerr << "Ошибка: переполнение при сложении!\n";
} else {
std::cout << "Результат: " << x + y << "\n";
}
return 0;
}
Умножение — сложнее
Для умножения проверка чуть хитрее: a > UINT_MAX / b (и не забудьте про деление на ноль).
bool will_mult_overflow(unsigned int a, unsigned int b) {
if (b == 0) return false; // Умножение на ноль — безопасно
return a > UINT_MAX / b;
}
Плюсы: Кроссплатформенность, полный контроль, никаких скрытых зависимостей.
Минусы: Нужно писать отдельные функции для каждой операции. Код становится громоздким.
Подход №2: Проверка после операции (Post-check)
Стандарт C++ гарантирует, что для unsigned типов результат операции — это (a + b) mod 2^N. Поэтому мы можем выполнить операцию, а потом проверить, не "обернулось" ли значение.
#include <iostream>
#include <limits>
template<typename T>
bool add_overflow(T a, T b, T& result) {
result = a + b;
return result < a; // Ключевой трюк
}
int main() {
unsigned int res;
if (add_overflow(4000000000u, 500000000u, res)) {
std::cout << "Переполнение зафиксировано!\n";
} else {
std::cout << "OK: " << res << "\n";
}
return 0;
}
Важно: Этот трюк работает, только если a и b одного беззнакового типа. Для uint32_t — отлично. Для смеси unsigned int и int — провал.
Ловушка для вычитания
Для вычитания условие обратное: result > a.
template<typename T>
bool sub_overflow(T a, T b, T& result) {
result = a - b;
return result > a;
}
Плюсы: Простота, один проход.
Минусы: Не сработает для умножения (там нужно проверять до, либо использовать больший тип). Также есть риск, что оптимизатор сломает логику при использовании volatile или constexpr.
Подход №3: Современный C++ — <limits> и встроенные функции компилятора
Начиная с C++11, мы можем использовать std::numeric_limits для написания универсальных шаблонов. Но настоящий прорыв — это built-in функции компилятора. Они работают на уровне машинных инструкций и дают максимальную производительность.
GCC и Clang: __builtin_add_overflow
Эти компиляторы предоставляют семейство функций:
__builtin_add_overflow(a, b, &result)__builtin_sub_overflow(a, b, &result)__builtin_mul_overflow(a, b, &result)
Они возвращают bool (true, если переполнение произошло) и работают с любыми целочисленными типами, включая signed.
#include <iostream>
#include <cstdint>
int main() {
uint32_t a = 4000000000;
uint32_t b = 500000000;
uint32_t result;
if (__builtin_add_overflow(a, b, &result)) {
std::cerr << "Переполнение!\n";
} else {
std::cout << "Результат: " << result << "\n";
}
return 0;
}
MSVC: SafeInt или _addcarry_u32
В Visual Studio есть свои интринсики, но для кроссплатформенности рекомендую библиотеку SafeInt (от Microsoft, открытый исходный код). Она делает ровно то же самое, но с красивым C++ интерфейсом.
#include <SafeInt.hpp>
int main() {
int x = 2147483647;
int y = 1;
try {
int result = SafeInt<int>(x) + y; // Бросит исключение при переполнении
} catch (const SafeIntException& e) {
std::cerr << "SafeInt поймал переполнение!\n";
}
return 0;
}