【问题标题】:Operator precedence and automatic promotion (to avoid overflow)运算符优先级和自动提升(避免溢出)
【发布时间】:2015-05-18 23:20:20
【问题描述】:

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

人为的例子:

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 个或更多值相乘以确保它们会自动提升?
(避免溢出)

或者这是未定义的行为?


注意事项...

  • 在这里使用size_t 不会防止溢出,它只是防止溢出该类型的最大值。
  • 在给出的示例中,让参数也可以是 size_t 是有意义的,但这不是这个问题的重点。

【问题讨论】:

  • 考虑将所有参数设为size_t,避免在函数中进行强制转换。如果您使用 int 参数,请考虑在相乘之前确保它们都是非负的(如果不是正数)——但是您可能需要 3 次转换才能安全,4 次才能系统/一致。
  • @Jonathan Leffler,是的,在这个例子中你的权利。但这不是问题的重点——输入可能是int,原因有很多——例如,图像格式可能使用整数。
  • 这是第一句话;这不是一个选择。因此,您继续看第二句话。
  • "但是你可能需要 3 次强制转换" - 我喜欢更简洁可能,如果这是编译器特定的(未定义) 行为。没关系。然后最好只添加演员表。但是如果 C 规范中有一个令人难忘的规则。意识到这一点是很好的,以便在审核代码时标记潜在的错误。
  • 这取决于您要承担的风险。你认为如果你省略一些强制转换,编译器有可能做出错误的决定吗?如果是这样,请不要冒险。当有整数溢出的机会时,现代编译器似乎想不遗余力地破坏代码。我不会冒险的。标准可能说“你应该没事”,但我没有确保这一点,因为我不喜欢冒险。这就是我制作 cmets 的原因,而不是答案。这还不够确定,不能作为答案。如果有人提出一个引用标准的答案,表明你是安全的(或不安全的),那就太好了。

标签: c casting integer-promotion


【解决方案1】:

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

  1. 使用“通常的算术转换”将两个操作数转换为相同的类型。

  2. 这就是结果的类型。

许多期望算术或枚举类型的操作数的二元运算符会导致转换并以类似的方式产生结果类型。目的是产生一个通用类型,这也是结果的类型。这种模式称为通常的算术转换 [注 1] [注 2]

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

结果类型不会自动加宽,以避免或降低溢出概率;操作数都转换为通用类型“这也是结果的类型”。因此,如果将两个 ints 相乘,结果将是 int,溢出将导致未定义的行为。 [注3]

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

C 标准的第 6.3.1.8 节(“常用算术转换”)和 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 是无符号的,计算将模2 执行N 其中N 是数字size_t 中的值位。因此,从避免溢出的意义上说,强制转换是不安全的,但它至少可以避免未定义的行为。


注意事项

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

  2. 移位运算符是例外,这就是为什么它说“许多二元运算符”。移位运算符的结果类型恰好是其左操作数的(可能提升的)类型,而与右操作数的类型无关。所有按位运算符都仅限于整数,因此涉及实数的“通常算术转换”部分不适用于这些运算符。

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

    涉及无符号操作数的计算永远不会溢出, 因为无法由生成的无符号整数类型表示的结果会以比结果类型可以表示的最大值大一的数字为模减少。 (C §6.2.5/9)

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

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

    如果 a 和 b 的值分别为 -32754 和 -15,则 a + b 之和会产生陷阱,而原始表达式不会;

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

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

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

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

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

    • 如果以上情况都不成立,则为有符号类型对应的无符号类型。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2011-06-21
    • 2020-03-05
    • 1970-01-01
    • 1970-01-01
    • 2021-03-29
    • 2013-07-31
    • 1970-01-01
    • 2023-03-07
    相关资源
    最近更新 更多