0

我一直在尝试创建一个尽可能完整的 Fraction 类,以自学 C++、类和相关的东西。除其他外,我想确保某种程度的“保护”免受浮点异常和溢出。

客观的:

避免常见运算中出现的算术运算中的溢出和浮点异常,消耗最少的时间/内存。如果避免是不可能的,那么至少检测它。

此外,这个想法是不要转换为更大的类型。这会产生一些问题(比如可能没有更大的类型)

我发现的案例:

  1. +、-、*、/、pow、root 上的溢出

    操作大多很简单(a并且bLong):

    • a+b:如果 LONG_MAX - b > a 则存在溢出。(还不够。a或者b可能是负面的)
    • ab:如果 LONG_MAX - a > -b 则存在溢出。(同上)
    • a*b:如果 LONG_MAX / b > a 则存在溢出。(如果 b != 0)
    • a/b:如果 a << b 可能会抛出 SIGFPE,如果 b << 0 则可能会溢出
    • pow(a,b): if (pow(LONG_MAX, 1.0/b) > a 那么有溢出。
    • pow(a,1.0/b):类似于 a/b
  2. 当 x = LONG_MIN(或等效项)时 abs(x) 溢出

    这很好笑。每个有符号类型都有一个范围 [-x-1,x] 的可能值。abs(-x-1) = x+1 = -x-1 因为溢出。这意味着存在 abs(x) < 0 的情况

  3. 具有大数字的 SIGFPE 除以 -1

    应用分子/gcd(分子,分母)时发现。有时 gcd 返回 -1,我得到一个浮点异常。

简单的修复:

  1. 在某些操作上很容易检查溢出。如果是这种情况,我总是可以转换为 double (有失去大整数精度的风险)。这个想法是找到一个更好的解决方案,而不是强制转换。

    在分数算术中,有时我可以进行额外的简化检查:为了解决 a/b * c/d(共素数),我可以先简化为共素数 a/d 和 c/b。

  2. 如果询问aorb是否 <0 或 > 0,我总是可以级联。不是最漂亮的。除了那个可怕的选择,我可以创建一个函数 neg() 来避免溢出
    T neg(T x){if (x > 0) return -x; else return x;},
    
  3. 我可以采取 gcd 的 abs(x) 和任何类似情况(任何地方 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 规则是一个很好的起点,但再次只讨论如何检测。

其他答案太笼统了,我想知道对于我正在查看的案例是否有更具体的答案。

4

1 回答 1

0

您需要区分浮点运算和积分运算

关于后者,对unsigned类型的操作通常不会溢出,除了除以零,这是 IIRC 定义的未定义行为。这与 C(++) 标准要求对无符号数使用二进制表示这一事实密切相关,这实际上使它们成为一个环。

相比之下,C(++) 标准允许signed数字的多种实现(符号+数量、1 的补码,或者最广泛使用的 2 的补码)。所以有符号溢出被定义为未定义的行为,可能是为了让编译器实现者有更多的自由来为他们的目标机器生成有效的代码。这也是您担心的原因abs():至少在 2 的补码表示中,没有大小等于最大负数的正数。请参阅CERT 规则进行详细说明。

在浮点方面SIGFPE,历史上一直被创造用于发出浮点异常信号。但是,鉴于当今处理器中算术单元的实现方式多种多样,SIGFPE应该将其视为报告算术错误的通用信号。例如,glibc 参考手册给出了可能的原因列表,明确包括被零整除

值得注意的是,根据ANSI/IEEE Std 754的浮点运算(我想今天最常用)是专门设计为一种防错的。这意味着,例如,当加法溢出时,它会给出无穷大的结果,并且通常会设置一个您可以稍后检查的标志。在进一步的计算中使用这个无限值是完全合法的,因为已经为仿射算术定义了浮点运算。这曾经是为了允许长时间运行的计算(在慢速机器上)即使在中间溢出等情况下也能继续。请注意,即使在仿射算术中也禁止某些操作,例如将无穷大除以无穷大或将无穷大减去无穷大。

所以底线是浮点计算通常不应该导致浮点异常。但是,您可以使用所谓的陷阱SIGFPE只要上述标志被提高,就会触发(或类似的机制)。

于 2018-02-02T16:01:35.960 回答