这个两增量表达式在 C 中定义得很清楚吗?

Is this two-increments expression well-defined in C?

提问人:Thomas Baruchel 提问时间:11/14/2023 更新时间:11/14/2023 访问量:121

问:

我想通过跳过所有具有特定位集的整数来遍历整数的 2 的幂。假设是 2 的某种幂(小于 ),我想跳过所有整数,以便 .我喜欢下面的 -loop,但我想知道它是否根据标准完美定义:tmtjj&m != 0for

for (int j=0; j < t; j += (++j) & m) {
    ...
}

这个想法是:

  • increment with 用于遍历所有整数j++j
  • 每当前一个增量设置了权重位时,也添加跳过整个块。mm

但是,由于同一表达式中有两个增量,因此我想确保它是完全正确的。

c 标准 预增量

评论

3赞 Eric Postpischil 11/14/2023
要跳过已设置的整数,可以使用 .打开该位,因此,如果进位在加法中到达它,它将传播到下一个位,然后加 1,然后关闭该位。mj = (j | m) + 1 & ~m;| m+ 1& ~m
1赞 Eric Postpischil 11/14/2023
(等效地,知道 的位是关闭的,我们可以用 代替 打开它,然后我们有 。而且,如果我们知道正在使用 2 的补码,则为 。所以我们可以使用 .然后,如果被缓存,我们只有一个常量和两个操作。mj+ m| mj = j + (m+1) & ~mm+1- ~mj = j - ~m & ~m~m
0赞 Fe2O3 11/14/2023
for( int x = 0; x < t; x += 1 + !!(x+1&m)*m )...任何有一定经验的人都可以理解,同时不需要极端的心理体操......
0赞 Fe2O3 11/14/2023
@EricPostpischil 在SO上,人们读到多少次“微优化是坏的!:-)
0赞 Eric Postpischil 11/14/2023
@Fe2O3:发布某事的次数并不能衡量其正确性。

答:

4赞 gulpr 11/14/2023 #1

这个程序的行为没有被定义为 (UB) *(谢谢 Eric)*在修改和分配之间没有序列点没有序列。

许多编译器会发出警告:

warning: unsequenced modification and access to 'j' [-Wunsequenced]
    |     for (int j=0; j < t; j += (++j) & m) {

即使没问题,我也强烈建议你不要写这种“hacky”表达式,因为它们对人类来说很难阅读,使得代码很难理解、调试和维护

3赞 dbush 11/14/2023 #2

该表达式调用未定义的行为,因为它包含对 的多个未序列写入。j += (++j) & mj

与其试图想出一个“聪明”的增量,不如在语句中执行增量并在循环中执行检查。for

for (int j=0; j < t; ++j) {
    if ((j&m) != 0) {
        // skip ahead
        j += m - 1;
        continue;
    }
    ...
}

评论

0赞 Thomas Baruchel 11/14/2023
虽然我同意编写一个简单易读的代码,但我认为您仍然可以跳过整个块,而不是像这里那样遍历所有整数。mcontinue
0赞 dbush 11/14/2023
@ThomasBaruchel 好点子。我添加了一行,跳到所需的数量。
3赞 Jonathan Leffler 11/14/2023 #3

如前所述,您尝试使用的表达式具有未定义的行为,因为 和 是未排序的。但是,您对要执行的操作的描述表明,使用应该执行所需的操作,而不会出现任何未定义的行为。+++=j += 1 + ((j + 1) & m)

#include <stdio.h>

int main(void)
{
    int m = 0x8;
    int t = 33;

    for (int j = 0; j < t; j += 1 + ((j + 1) & m))
        printf("%d\n", j);

    return 0;
}

输出:

0
1
2
3
4
5
6
7
16
17
18
19
20
21
22
23
32

我认为这符合你的描述。因为是 8,所以它会立即跳过值 8..15 和 24..31,它们是 0..32 范围内设置了位的值。m

请注意,仅当 的初始值不包括 标识的位时,这才有效。如果将 start 从 更改为 ,则得到输出 、 。当然,可以修改代码来处理这个问题,但这有点棘手,确切地说如何做到这一点取决于尚未说明的要求(预期的输出是什么?jm09918..2332

旁注:如果您的问题包括您期望的输出的示例输出,这将很有帮助,类似于我提供的内容。请阅读有关如何创建 MCVE(最小、完整、可验证示例 — 或 MRE 或 SO 现在使用的任何名称)的信息 或 SSCCE(简短、自包含、正确的示例——相同的想法只是不同的名称)。

1赞 Lundin 11/14/2023 #4

这是未定义的行为。该标准的相关部分是:

C17 6.5 §2(表达式):

如果标量对象上的副作用相对于 相同的标量对象或使用同一标量对象的值进行值计算,行为 未定义。如果表达式的子表达式有多个允许的顺序,则 如果在任何排序中发生这种未排序的副作用,则行为是未定义的。

C17 6.5.16 §3(赋值运算符):

旁边 更新左操作数的存储值的效果在值计算之后排序 左操作数和右操作数。操作数的计算是未排序的。

C17 6.5.16.2 §3(复合分配):

形式的复合赋值等价于简单赋值表达式,只是左值 E1 只计算一次E1 op= E2E1 = E1 op (E2)

基于此:

  • 一些味道是UB。
    表达式等效于 。值计算与同一对象的副作用有关是无序的。UB根据“同一对象的副作用未排序的相对值计算”。
    j + ++jj += (++j) & mj = j + ((++j) & m)j +...++j

  • 一些味道是UB。
    操作数的计算是未排序的:vs 。UB 根据发生未排序副作用的多个允许顺序排序。
    j = ++j=j = ...j + ((++j) & m)++j