传统编译器中是否可以将逻辑取反实现为位取反?

3

我刚刚发现了一段测试标志的旧代码,它是这样的:

if( some_state & SOME_FLAG )

目前为止,一切都很好!
但在代码后面,我看到了一个不当的否定。

if( ! some_state & SOME_FLAG )

我的理解是,它被解释为(! some_state) & SOME_FLAG,这可能是一个bug,gcc会逻辑地抱怨-Wlogical-not-parentheses... 尽管在过去,如果某个遗留编译器将!some_state实现为~some_state,那么它最终可能会起作用。有人知道是否可能是这种情况吗? 编辑 sme_state声明为int(假设在目标架构上是32位,2补码)。
SOME_FLAG是一个常量,设置为单个位0x00040000,因此SOME_FLAG & 1 == 0

1
每个踩票都应该有一个解释或指导,请务必给我启发。 - aka.nice
你确定代码按预期工作了吗?也许这个错误对用户没有任何影响? - Bodo
这正是我的问题:这曾经有效吗?由于逻辑非操作现在会回答0或1,而SOME_FLAG类似于0x4,我确信if(!some_state&SOME_FLAG)始终为false,并且可以完全被现代编译器消除。 - aka.nice
1
我在我工作的代码库中偶尔看到类似的代码。每次都是一个错误。我非常倾向于认为这也是你的代码中的一个错误。 - Jonathan Leffler
2
@aka.nice 时不时地我会发现一些 bug,然后问自己这个 bug 是怎么工作的。根据软件的复杂程度,用户可能不会注意到一个 bug,可能是因为它的影响只在罕见情况下可见,或者可能是因为其他代码正在解决这个问题。 - Bodo
显示剩余2条评论
4个回答

4
逻辑非运算和位非运算从未等同过。没有符合规范的编译器可以将一个实现为另一个。例如,1的位非不是0,所以~1 != !1
确实,表达式! some_state & SOME_FLAG等同于(! some_state) & SOME_FLAG,因为逻辑非具有比位与更高的优先级。这确实有点可疑,但原始代码并不一定错误。无论如何,程序出现这种情况的可能性更大,而不是任何C实现在标准化之前就已经按照当前标准要求评估了原始表达式。
由于表达式(! some_state) & SOME_FLAG!(some_state & SOME_FLAG)有时会计算出相同的值,特别是如果SOME_FLAG恰好扩展为1,那么它们是不等价的,它们的差异在程序实际执行期间也可能不会显现。

1
这个反例是我在答案中看到的最简单的一个,现在我明白了我的猜测是愚蠢的。有时候,尽管有所有的证据,我也不想相信我看到的错误。 - aka.nice

3

尽管在1989年之前没有标准,因此编译器可以随意操作,但据我所知,没有一个编译器会这样做;如果你希望人们使用你的编译器,改变运算符的含义是不明智的。

很少有理由编写像 (!foo & FLAG_BAR) 这样的表达式;如果 FLAG_BAR 是奇数,则结果仅为 !foo,如果它是偶数,则始终为零。你发现的几乎肯定只是一个错误。


3

对于传统编译器来说,无法将 ! 实现为按位取反,因为这种方法在对被取反的值位于 {0, 0xFF...FF} 之外的情况下会产生错误的结果。

标准要求对于任何非零值 x!x 的结果应该为零。因此,对于数字 1 进行 ! 操作将产生非零的结果 0xFF..FFFE。

只有当 SOME_FLAG 被设置为 1 时,传统代码才能按预期工作。


3
尽管这适用于C语言,但有趣的是,在早期版本的VB中,布尔值是16位整数,其中false为0,true为0xffff,因此按位取反的效果与您预期的相同。 - Eric Lippert
我有一个模糊的记忆,80年代某个C编译器也将true实现为0xffffffff,在ANSI之前。但也许我的记忆是错误的... - aka.nice

0

让我们从最有趣(也是最不明显)的部分开始:gcc逻辑上会使用-Wlogical-not-parentheses进行警告。这是什么意思呢?

C语言有两个不同的运算符,它们看起来很相似(但行为不同,目的也非常不同)- &是按位与,&&是布尔与。不幸的是,这导致了打字错误,就像当你想要输入==时却输入了=一样,会引起问题,因此一些编译器(如GCC)决定警告人们关于“在条件中使用没有括号的&”(即使它是完全合法的),以减少打字错误的风险。

现在...

您正在展示使用&的代码(并未展示使用&&的代码)。这意味着some_state不是布尔值,而是数字。更具体地说,它意味着some_state中的每个位可能是完全独立和无关的。

举个例子,假设我们正在实现一个吃豆人游戏,并需要一种紧凑的方式来存储每个级别的地图。我们决定地图中的每个方块可能是墙或不是,可能是收集的点或不是,可能是能量药片或不是,可能是樱桃或不是。有人建议这可以是一个字节数组,就像这样(假设地图宽30个方块,高20个方块):

#define IS_WALL         0x01
#define HAS_DOT         0x02
#define HAS_POWER_PILL  0x04
#define HAS_CHERRY      0x08

uint8_t level1_map[20][30] = { ..... };

如果我们想知道一个瓷砖是否可以安全地进入(没有墙壁),我们可以这样做:
    if( level1_map[y][x] & IS_WALL == 0) {

相反地,如果我们想知道一个瓷砖是否是墙,我们可以执行以下任何操作:

    if( level1_map[y][x] & IS_WALL != 0) {

    if( !level1_map[y][x] & IS_WALL == 0) {

    if( level1_map[y][x] & IS_WALL == IS_WALL) {

因为它是哪一个都没有区别。

当然(为了避免打错字的风险),GCC可能会(或可能不会)警告其中一些。


假设 level1_map[y][x] == 0x2,那么 (!level1_map[y][x]) == 0,然后 ((!level1_map[y][x]) & IS_WALL) == 0,所以我认为第二种情况不是测试墙的正确方式。 - aka.nice
如果你的狗叫了起来,看看外面是不是只有一只猫,或者可能是一个小偷。如果你的编译器出现问题,也一样,总是去检查你的源代码。我更喜欢严谨的编译器。遗留代码的问题在于它会看到很多猫并且会频繁报错。 - aka.nice
@aka.nice:啊,你说得对,应该是 if( ~level1_map[y][x] & IS_WALL == 0) { - Brendan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接