Как правильно избежать SIGFPE и переполнения при арифметических операциях

Я пытался создать класс Fraction настолько полно, насколько это возможно, чтобы самостоятельно изучать C++, классы и связанные с ними вещи. Помимо прочего, я хотел обеспечить некоторый уровень "защиты" от исключений и переполнений с плавающей запятой.

Задача:

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

Кроме того, идея состоит в том, чтобы не приводить к более крупным типам Это создает кучу проблем (например, не может быть более крупного типа)

Случаи, которые я нашел:

  1. Переполнение на +, -, *, /, 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
  2. Переполнение абс (х), когда х = LONG_MIN (или эквивалентный)

    Это смешно. Каждый тип со знаком имеет диапазон [-x-1,x] возможных значений. abs(-x-1) = x+1 = -x-1 из-за переполнения. Это означает, что есть случай, когда abs(x) <0

  3. SIGFPE с большими числами, деленными на -1

    Найдено при применении числитель / GCD (числитель, знаменатель). Иногда gcd возвращает -1, и я получаю исключение с плавающей запятой.

Простые исправления:

  1. На некоторых операциях легко проверить переполнение. Если это так, я всегда могу привести к удвоению (с риском потери точности по большим целым числам). Идея состоит в том, чтобы найти лучшее решение без кастинга.

    В арифметике дробей иногда я могу выполнить дополнительную проверку на упрощения: чтобы решить a/b * c/d (сопряжения), я могу сначала привести к сопряжениям a/d и c/b.

  2. Я всегда могу сделать каскад, если спрашивает, если a или же b <0 или> 0. Не самый красивый. Помимо этого ужасного выбора, Я могу создать функцию neg(), которая позволит избежать этого переполнения
    T neg(T x){if (x > 0) return -x; else return x;},
    
  3. Я могу взять 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

Другие ответы слишком общие, и мне интересно, есть ли какие-либо ответы, более конкретные для рассматриваемых мной случаев.

1 ответ

Вы должны различать операции с плавающей запятой и интегральные операции.

Что касается последнего, операции по unsigned типы обычно не переполняются, за исключением деления на ноль, что по определению IIRC является неопределенным поведением. Это тесно связано с тем, что стандарт C(++) предписывает двоичное представление для чисел без знака, что фактически делает их кольцом.

Напротив, стандарт C(++) допускает несколько реализаций signed числа (знак + величина, 1 дополнение или, наиболее широко используемый, 2 дополнение). Таким образом, переполнение со знаком определяется как неопределенное поведение, возможно, чтобы дать разработчикам компилятора больше свободы для генерации эффективного кода для своих целевых машин. Это и есть причина вашего беспокойства abs(): По крайней мере, в представлении дополнения 2 нет положительного числа, равного по величине наибольшему отрицательному числу по величине. Обратитесь к правилам CERT для уточнения.

На стороне с плавающей точкой SIGFPE исторически был придуман для сигнализации об исключениях с плавающей запятой. Однако, учитывая многообразие реализаций арифметических единиц в процессорах в настоящее время, SIGFPE следует рассматривать общий сигнал, сообщающий об арифметических ошибках. Например, в справочном руководстве по glibc приведен список возможных причин, явно включающий целочисленное деление на ноль.

Стоит отметить, что операции с плавающей запятой в соответствии со стандартом ANSI / IEEE Std 754, который, как я полагаю, наиболее часто используется сегодня, специально разработаны для того, чтобы быть своего рода защищенным от ошибок. Это означает, что, например, когда дополнение переполняется, оно дает результат бесконечности и обычно устанавливает флаг, который вы можете проверить позже. Вполне допустимо использовать это бесконечное значение в дальнейших вычислениях, поскольку операции с плавающей запятой были определены для аффинной арифметики. Когда-то это должно было позволить продолжительным вычислениям (на медленных машинах) продолжаться даже с промежуточными переполнениями и т. Д. Обратите внимание, что некоторые операции запрещены даже в аффинной арифметике, например, деление бесконечности на бесконечность или вычитание бесконечности на бесконечность.

Итак, суть в том, что вычисления с плавающей запятой обычно не должны вызывать исключения с плавающей запятой. Тем не менее, вы можете иметь так называемые ловушки, которые вызывают SIGFPE (или аналогичный механизм), который должен срабатывать всякий раз, когда вышеупомянутые флаги становятся поднятыми.

Другие вопросы по тегам