Как правильно избежать SIGFPE и переполнения при арифметических операциях
Я пытался создать класс Fraction настолько полно, насколько это возможно, чтобы самостоятельно изучать C++, классы и связанные с ними вещи. Помимо прочего, я хотел обеспечить некоторый уровень "защиты" от исключений и переполнений с плавающей запятой.
Задача:
Избегайте переполнения и исключений с плавающей запятой в арифметических операциях, встречающихся в обычных операциях, затрачивая меньше времени и памяти. Если избежать невозможно, по крайней мере, обнаружить его.
Кроме того, идея состоит в том, чтобы не приводить к более крупным типам Это создает кучу проблем (например, не может быть более крупного типа)
Случаи, которые я нашел:
Переполнение на +, -, *, /, pow, root
Операции в основном прямые (
a
а такжеb
являютсяLong
):- a + b: если LONG_MAX - b > a, то есть переполнение. (недостаточно.
a
или жеb
могут быть негативы) - ab: если LONG_MAX - a > -b, то есть переполнение. (Там же)
- a * b: если LONG_MAX / b > a, то есть переполнение. (если b! = 0)
- a/b: может вызвать SIGFPE, если a << b, или переполнение, если b << 0
- pow (a, b): если (pow(LONG_MAX, 1.0/b) > a, то есть переполнение.
- pow (a, 1.0 / b): аналогично a/b
- a + b: если LONG_MAX - b > a, то есть переполнение. (недостаточно.
Переполнение абс (х), когда х = LONG_MIN (или эквивалентный)
Это смешно. Каждый тип со знаком имеет диапазон [-x-1,x] возможных значений. abs(-x-1) = x+1 = -x-1 из-за переполнения. Это означает, что есть случай, когда abs(x) <0
- SIGFPE с большими числами, деленными на -1
Найдено при применении числитель / GCD (числитель, знаменатель). Иногда gcd возвращает -1, и я получаю исключение с плавающей запятой.
Простые исправления:
- На некоторых операциях легко проверить переполнение. Если это так, я всегда могу привести к удвоению (с риском потери точности по большим целым числам). Идея состоит в том, чтобы найти лучшее решение без кастинга.
В арифметике дробей иногда я могу выполнить дополнительную проверку на упрощения: чтобы решить a/b * c/d (сопряжения), я могу сначала привести к сопряжениям a/d и c/b.
Я всегда могу сделать каскад, если спрашивает, еслиЯ могу создать функцию neg(), которая позволит избежать этого переполненияa
или жеb
<0 или> 0. Не самый красивый. Помимо этого ужасного выбора,T neg(T x){if (x > 0) return -x; else return x;},
- Я могу взять abs(x) из gcd и любой подобной ситуации (где угодно x > LONG_MIN)
Я не уверен, что 2. и 3. являются лучшими решениями, но кажется достаточно хорошим. Я публикую их здесь, так что, возможно, у кого-нибудь есть лучший ответ.
Уродливые исправления
В большинстве операций мне нужно выполнить много дополнительных операций, чтобы проверить и избежать переполнения. Вот где я почти уверен, что могу выучить одну или две вещи.
Пример:
Fraction Fraction::operator+(Fraction f){
double lcm = max(den,f.den);
lcm /= gcd(den, f.den);
lcm *= min(den,f.den);
// a/c + b/d = [a*(lcm/d) + b*(lcm/c)] / lcm //use to create normal fractions
// a/c + b/d = [a/lcm * (lcm/c)] + [b/lcm * (lcm/d)] //use to create fractions through double
double p = (double)num;
p *= lcm / (double)den;
double q = (double)f.num;
q *= lcm / (double)f.den;
if(lcm >= LONG_MAX || (p + q) >= LONG_MAX || (p + q) <= LONG_MIN){
//cerr << "Aproximating " << num << "/" << den << " + " << f.num << "/" << f.den << endl;
p = (double)num / lcm;
p *= lcm / (double)den;
q = (double)f.num / lcm;
q *= lcm / (double)f.den;
return Fraction(p + q);
}
else
return normal(p + q, (long)lcm);
}
Каков наилучший способ избежать переполнения этих арифметических операций?
Редактировать: На этом сайте есть несколько вопросов, которые очень похожи, но они не совпадают (обнаружение вместо того, чтобы избежать, без знака, а не со знаком, SIGFPE в конкретных ситуациях, не связанных с).
Проверяя все из них, я нашел несколько ответов, которые после модификации могут быть полезны, чтобы дать правильный ответ, например:
- Обнаружение переполнения в неподписанном добавлении (не мой случай, я работаю с подписанным):
uint32_t x, y; uint32_t value = x + y; bool overflow = value < x; // Alternatively "value < y" should also work
Обнаружение переполнения в подписанных операциях. Это может быть слишком общим, с большим количеством веток и не обсуждать, как избежать переполнения.
Правила CERT, упомянутые в ответе, являются хорошей отправной точкой, но опять же обсуждают только то, как их обнаружить.
Другие ответы слишком общие, и мне интересно, есть ли какие-либо ответы, более конкретные для рассматриваемых мной случаев.
1 ответ
Вы должны различать операции с плавающей запятой и интегральные операции.
Что касается последнего, операции по unsigned
типы обычно не переполняются, за исключением деления на ноль, что по определению IIRC является неопределенным поведением. Это тесно связано с тем, что стандарт C(++) предписывает двоичное представление для чисел без знака, что фактически делает их кольцом.
Напротив, стандарт C(++) допускает несколько реализаций signed
числа (знак + величина, 1 дополнение или, наиболее широко используемый, 2 дополнение). Таким образом, переполнение со знаком определяется как неопределенное поведение, возможно, чтобы дать разработчикам компилятора больше свободы для генерации эффективного кода для своих целевых машин. Это и есть причина вашего беспокойства abs()
: По крайней мере, в представлении дополнения 2 нет положительного числа, равного по величине наибольшему отрицательному числу по величине. Обратитесь к правилам CERT для уточнения.
На стороне с плавающей точкой SIGFPE
исторически был придуман для сигнализации об исключениях с плавающей запятой. Однако, учитывая многообразие реализаций арифметических единиц в процессорах в настоящее время, SIGFPE
следует рассматривать общий сигнал, сообщающий об арифметических ошибках. Например, в справочном руководстве по glibc приведен список возможных причин, явно включающий целочисленное деление на ноль.
Стоит отметить, что операции с плавающей запятой в соответствии со стандартом ANSI / IEEE Std 754, который, как я полагаю, наиболее часто используется сегодня, специально разработаны для того, чтобы быть своего рода защищенным от ошибок. Это означает, что, например, когда дополнение переполняется, оно дает результат бесконечности и обычно устанавливает флаг, который вы можете проверить позже. Вполне допустимо использовать это бесконечное значение в дальнейших вычислениях, поскольку операции с плавающей запятой были определены для аффинной арифметики. Когда-то это должно было позволить продолжительным вычислениям (на медленных машинах) продолжаться даже с промежуточными переполнениями и т. Д. Обратите внимание, что некоторые операции запрещены даже в аффинной арифметике, например, деление бесконечности на бесконечность или вычитание бесконечности на бесконечность.
Итак, суть в том, что вычисления с плавающей запятой обычно не должны вызывать исключения с плавающей запятой. Тем не менее, вы можете иметь так называемые ловушки, которые вызывают SIGFPE
(или аналогичный механизм), который должен срабатывать всякий раз, когда вышеупомянутые флаги становятся поднятыми.