运算符优先级和自动提升(避免溢出)

Operator precedence and automatic promotion (to avoid overflow)

以字节为单位查找一些数据的大小是一个常见的操作。

人为的例子:

char *buffer_size(int x, int y, int chan_count, int chan_size)
{
    size_t buf_size = x * y * chan_count * chan_size;  /* <-- this may overflow! */
    char *buf = malloc(buf_size);
    return buf;
}

这里明显的错误是整数会溢出(例如一个 23171x23171 RGBA 字节缓冲区)。

3个或更多个值相乘的提升规则是什么?
(一对值相乘很简单)

我们可以谨慎行事,只需施放:

size_t buf_size = (size_t)x * (size_t)y * (size_t)chan_count * (size_t)chan_size;

另一种选择是添加括号以确保乘法和提升的顺序是可预测的(并且对之间的自动提升按预期工作)...

size_t buf_size = ((((size_t)x * y) * chan_count) * chan_size;

...这行得通,但我的问题是。


是否有确定性的方法将 3 个或更多值相乘以确保它们自动提升?
(避免溢出)

或者这是未定义的行为?


注释...

在C(和C++)中,算术运算符的类型确定如下:

  1. 两个操作数被转换为相同的类型,使用"usual arithmetic conversions".

  2. 就是结果的类型

Many binary operators that expect operands of arithmetic or enumeration type cause conversions and yield result types in a similar way. The purpose is to yield a common type, which is also the type of the result. This pattern is called the usual arithmetic conversions [Note 1] [Note 2]

没有其他规则,因此具有两个或更多运算符的表达式没有特殊情况。根据语法,每个操作都是独立输入的。

结果类型不会自动加宽以避免或减少溢出的概率;操作数都转换为通用类型 "which is also the type of the result"。因此,如果将两个 int 相乘,结果将是 int 并且溢出将导致未定义的行为。 [注3]

语言的语法精确定义了完整表达式的分组方式,并且要求评估符合语法。表达式 a + b + c 必须与表达式 (a + b) + c 具有相同的结果,因为语法需要分组。编译器可以根据需要重新安排计算,前提是它可以证明所有有效输入的结果在语义上是相同的。但它不能决定更改任何运算符的结果类型。 a + b + c 的类型必须是将通常的算术转换应用于 ab 的类型,然后再次将它们应用于该类型和 c 的类型. [注4]

常见的算术转换在 C 标准的 §6.3.1.8 ("Usual arithmetic conversions") 和 C++ 的 §5(表达式)简介的第 10 段中有详细说明。粗略地说,它是这样的:

  1. 如果两个操作数都是浮点数,则两个操作数都转换为两种类型中较宽的一个;如果一个操作数是浮点数,则另一个被转换为该浮点数类型。

  2. 否则,如果两个操作数都是有符号整数类型,则它们都转换为两个类型中最宽的类型并且int

  3. 否则,如果两个操作数都是无符号整数类型,至少与unsigned int一样大,它们都被转换为两个类型中更宽的一个。

[注5]

现在,以 a * b * c * d 为例,其中 abcd 都是 int 并且愿望是制作一个 size_t.

在句法上,该表达式等同于 (((a * b) * c) * d),并且通常的算术转换相应地应用到操作中。如果您使用转换 ((size_t)a * b * c * d) 将 a 转换为 size_t,转换将被应用,就好像它被括号括起来一样。所以 (size_t)a * b 的操作数和结果将是 size_t,因此 (size_t)a * b * c 的结果也是 (size_t)a * b * c * d。换句话说,所有操作数都将转换为无符号 size_t 值,并且所有乘法都将作为无符号 size_t 乘法执行。这是明确定义的,但如果任何值恰好为负数,则可能毫无意义。

第二次或第三次乘法可能会超过 size_t 的容量,但由于 size_t 是无符号的,因此计算将以 2N[=150 为模执行=] 其中 Nsize_t 中的值位数。因此,强制转换在避免溢出的意义上是不安全的,但它至少避免了未定义的行为。


备注

  1. 引用自 C++ 标准,§5,第 10 段。C 标准在 §6.3.1.8 中有一个稍微复杂的版本,因为 C11 包括复杂的算术类型。对于整数(和非复数浮点数)操作数,C 和 C++ 具有相同的语义。

  2. 移位运算符是例外,这就是它说 "many binary operators" 的原因。移位运算符的结果类型恰好是其左操作数的(可能提升的)类型,而与右操作数的类型无关。所有按位运算符都限于整数,因此 "usual arithmetic conversions" 中涉及实数的部分不适用于这些运算符。

  3. 如果将两个 unsigned int 相乘,结果将是 unsigned int 并且为所有值定义计算:

    A computation involving unsigned operands can never overflow, because a result that cannot be represented by the resulting unsigned integer type is reduced modulo the number that is one greater than the largest value that can be represented by the resulting type. (C §6.2.5/9)

  4. C 和 C++ 标准在这一点上都非常清楚,并包含示例来说明这一点。一般来说,有符号整数和浮点运算符都不是关联的,因此只有在计算仅涉及无符号整数运算时,才可能重新组合和重新排列计算。

    C 标准第 5.1.2.3 节中的示例 6 和 C++ 标准第 1.9 节的第 9 段中出现了禁止整数算术重组的情况示例。 (这是同一个例子。)假设我们有一台具有 16 位 ints 的机器,其中有符号溢出导致陷阱。在那种情况下,a = a + 32760 + b + 5; 不能重写为 a = (a + b) + 32765;:

    if the values for a and b were, respectively, −32754 and −15, the sum a + b would produce a trap while the original expression would not;

  5. 那些是简单的,不麻烦的案例。通常你应该尽量避免其他的,但为了记录:

    一个。在上述情况发生之前,如果任一操作数的类型小于 int,则该操作数将被提升为 intunsigned int。通常,它会被提升为 int,即使它是未签名的。只有当 int 的宽度不足以表示该类型的所有值时,才会将操作数提升为 unsigned int。例如,在大多数架构上,unsigned char 操作数将被提升为 int,而不是 unsigned int(尽管 charint 的架构相同宽度是可能的,它们不常见。)

    b。最后,如果一种类型是有符号的,另一种是无符号的,那么它们都会被转换为:

    • unsigned 类型,如果它至少与有符号类型一样宽。 (例如 unsigned int * int => unsigned int

    • signed 类型,如果它足够宽以容纳无符号类型的所有值。 (例如 unsigned int * long long => long long 如果 long longint 宽)

    • 有符号类型对应的无符号类型如果none以上情况成立