在C++中使用位运算符处理布尔值

108

在C++中,对于“bool”值使用位运算符&、|和^是否有任何不合适的原因?

有时我遇到想要恰好满足两个条件之一(异或)的情况,所以我会将^操作符放入条件表达式中。我有时也希望所有条件都被评估,无论结果是真还是假(而不是短路),所以我使用&和|。有时我需要累加布尔值,这时&=和|=就非常有用。

当我这样做时,可能会引起一些人的反感,但代码仍然意义明确且更加简洁。是否有任何不应该对布尔值使用这些运算符的原因?是否有任何现代编译器会产生错误的结果?


如果您在提出此问题后已经学习了足够多的C++知识,那么您应该回来取消接受当前答案,并接受Patrick Johnmeyer的答案,因为他正确地解释了短路差异。 - codetaku
一般性的评论,因为人们质疑这是否是一个好主意。如果你关心单元测试的分支覆盖率,那么减少分支是可取的。位运算符在这方面非常有用。 - qznc
9个回答

71

||&&是布尔运算符,内置的运算符保证只会返回truefalse。没有其他值。

|&^ 是按位运算符。当你操作的数字域只有 1 和 0 时,它们实际上是相同的,但在布尔值不严格为 1 和 0 的情况下,例如 C 语言,你可能会得到一些意外的结果。例如:

BOOL two = 2;
BOOL one = 1;
BOOL and = two & one;   //and = 0
BOOL cand = two && one; //cand = 1

然而,在C++中,bool类型被保证只能是truefalse(它们分别隐式转换为10),因此从这个角度来看,它不太值得担心,但人们并不习惯在代码中看到这样的东西,这就是不这样做的好理由。只需说出b = b && x,然后完成即可。


1
回想起来,那有点傻。无论如何,我改变了例子并使用&&解决了问题。我发誓我以前有时用过^^,但这显然不是这种情况。 - Patrick
3
C语言自10年前开始就有了bool类型,它只能是0或1。_Bool - Johannes Schaub - litb
Patrick,既然你已经有了被接受的答案,如果你能添加一些关于使用&=|=累加的说明就更好了。在阅读整个线程并进行测试后,我检查并了解到gcc在bool passed_tests_ = true; passed_tests_ &= 8;之后会返回false,这意味着我的某个测试对后续编辑很脆弱...(寻找这个问题是我来到这里的原因) - sage
24
即使是布尔值,它们也不相同,因为它们不会短路。 - aaronman
5
我强烈认为在将"When the domain of numbers you operate on is just 1 and 0, then they are exactly the same"改成"When the domain of numbers you operate on is just 1 and 0, the only difference is that the bitwise operators do not short-circuit"之前,这不应该被接受为正确答案。前者陈述是完全错误的,因此我已经对该答案进行了贬值。 - codetaku
显示剩余2条评论

37

两个主要原因。简而言之,请仔细考虑;可能有一个很好的理由,但如果有的话,请在您的注释中非常明确,因为它可能很脆弱,并且正如您自己所说,人们通常不习惯看到这样的代码。

按位异或!=逻辑异或(除了0和1)

首先,如果您对除falsetrue(或作为整数的01)之外的值进行操作,则^运算符可能引入与逻辑异或不等效的行为。例如:

int one = 1;
int two = 2;

// bitwise xor
if (one ^ two)
{
  // executes because expression = 3 and any non-zero integer evaluates to true
}

// logical xor; more correctly would be coded as
//   if (bool(one) != bool(two))
// but spelled out to be explicit in the context of the problem
if ((one && !two) || (!one && two))
{
  // does not execute b/c expression = ((true && false) || (false && true))
  // which evaluates to false
}

首先,需要感谢用户@Patrick首先表达了这个概念。

运算顺序

其次,作为按位运算符,|&^不会短路。此外,即使使用明确的括号,单个语句中链接多个按位运算符也可以被优化编译器重新排序,因为这三个操作通常是可交换的。如果操作的顺序很重要,这一点非常重要。

换句话说

bool result = true;
result = result && a() && b();
// will not call a() if result false, will not call b() if result or a() false

不一定会得到相同的结果(或结束状态),如下所示:

bool result = true;
result &= (a() & b());
// a() and b() both will be called, but not necessarily in that order in an
// optimizing compiler

这是特别重要的,因为您可能无法控制方法a()b(),或者其他人可能会在以后更改它们,而不理解依赖关系,从而导致严重(通常仅在发布构建中出现)错误。


1
如果在一个情况下进行了bool转换而在另一个情况下没有进行转换,那么这并不是一个公平的比较。在两种情况下都进行bool转换才能使其正常工作,这也是为什么它很脆弱(因为你必须记住它)。 - Greg Rogers
短路的解释是为什么这应该绝对成为被接受的答案;帕特里克的回答(呃...另一个名叫帕特里克的人)完全错误,因为他说“当你操作的数字域只有1和0时,它们完全相同”。 - codetaku

14

抬起眉毛应该已经足以告诉你停止这样做了。你编写代码不是为了编译器,而是首先为了你的同行程序员,然后才为了编译器。即使编译器可以正常工作,也不希望让其他人感到惊讶 - 按位运算符用于位操作而不是布尔值。
我猜你也会用叉子吃苹果吧?它可以用,但会让人感到惊讶,所以最好别这样做。


3
是的,这应该是最佳答案。遵循最少惊讶原则。做出不寻常的事情会使代码更难阅读和理解,而代码被阅读的次数远远超过它被编写的次数。不要使用可爱的技巧。 - Eloff
1
我在这里是因为我的同事在bool类型中使用了位运算符“and”。现在我正在花费时间尝试理解这是否正确。如果他没有这样做,我现在会做更有用的事情 -_-。如果你重视同事的时间,请不要在bool类型中使用位运算符! - anton_rh

12

我认为

a != b

是你想要的。


1
这是正确的,+1。但是没有赋值版本的运算符(!==),所以如果你要计算一系列bool值的异或,你需要在循环体中写acc = acc!= condition(i);。编译器可能可以像存在!==一样高效地处理这个问题,但有些人可能觉得它看起来不太好,更喜欢将布尔值作为整数相加,然后测试和是否为奇数。 - Marc van Leeuwen

10

位运算操作的缺点

您提出了问题:

“在C ++中,对于“bool”值,使用按位运算符&,|和^是否有任何理由不使用?”

是的,逻辑运算符即内置的高级布尔运算符!, 即!&&||,具有以下优点:

  • 保证将参数转换为bool,即01的序数值。

  • 保证短路评估,其中表达式的评估在知道最终结果后立即停止。
    这可以解释为一个三值逻辑,包括不确定

  • 可读性更好的文本等效项notandor,即使我自己不使用它们。
    正如评论者Antimony在评论中指出的那样,位级别运算符也有替代标记,即bitandbitorxorcompl,但我认为这些标记不如andornot易读。

简而言之,高级运算符的每个优点都是位级别运算符的缺点。

特别地,由于按位运算符缺乏将参数转换为0/1的功能,因此例如1 & 20,而1 && 2true。另外,按位异或^也可能出现问题。将1和2视为布尔值时,它们是相同的,即true,但将它们视为位模式时,它们是不同的。


如何在C++中表示逻辑上的 either/or

您提供了一些背景信息:

“有时我遇到情况,我只想要两个条件中的一个为真(XOR),所以我只需将^运算符放入条件表达式中。”

好吧,位运算符的优先级更高,比逻辑运算符更高。这意味着在混合表达式中,例如

a && b ^ c

你会得到一个也许出乎意料的结果 a && (b ^ c)

相反,只需写

(a && b) != c
表达更简洁地表达您的意思。
对于多个参数的 either/or,没有 C++ 运算符可以完成此工作。例如,如果您写 a ^ b ^ c,那么这不是一个表达式,表示“abc 中的任意一个为真”。相反,它表示“abc 中奇数个为真”,这可能是其中一个或全部 3 个…
要表达通用的 abc 的 either/or,只需编写
(a + b + c) == 1

或者,对于非bool类型的参数,将它们转换为bool:

(!!a + !!b + !!c) == 1

使用 &= 累积布尔结果。

你进一步解释说:

“有时我也需要累积布尔值,&=|=? 可以非常有用。”

好的,这相当于检查是否分别满足所有条件或任何条件,并且德摩根定律告诉您如何从一个转换为另一个。也就是说,您只需要其中之一。您原则上可以使用 *= 作为 &&= 运算符(因为像老乔治·布尔一样发现,逻辑 AND 可以非常容易地表示为乘法),但我认为那可能会使代码维护者困惑甚至误导。

同时考虑:

struct Bool
{
    bool    value;

    void operator&=( bool const v ) { value = value && v; }
    operator bool() const { return value; }
};

#include <iostream>

int main()
{
    using namespace std;

    Bool a  = {true};
    a &= true || false;
    a &= 1234;
    cout << boolalpha << a << endl;

    bool b = {true};
    b &= true || false;
    b &= 1234;
    cout << boolalpha << b << endl;
}

使用Visual C ++ 11.0和g ++ 4.7.1时的输出:

true
false

结果不同的原因是bitlevel &= 没有将其右侧参数转换为 bool

那么,您需要使用&= 的哪个结果?

如果是前者true,则最好定义一个运算符(如上所述)或命名函数,或使用右侧表达式的显式转换,或完整地编写更新。


位运算符也有文本等效形式。 - Antimony
@Antimony:一开始我没理解那个评论,但是确实,位运算有替代的符号bitandbitorxorcompl。我想这就是为什么我使用了“可读性”这个限定词。当然,例如compl 42的可读性是主观的。;-) - Cheers and hth. - Alf
据我所知,逻辑运算符是可以与其他参数类型一起重载的(尽管它们通常不会这样做),因此第一个观点“将参数转换为bool的保证”只是约定的结果,而不是C++语言实际给出的保证。 - Marc van Leeuwen
@MarcvanLeeuwen:可能是您的视觉子系统没有注意到“内置的高级布尔运算符!、&&和||”。您是正确的,这些运算符可以被重载,然后主要问题是语义会发生微妙的变化,不再进行短路评估。但这是重载的问题,而不是内置运算符的问题。 - Cheers and hth. - Alf
没错(我漏了这个)。但是,说那些运算符只接受布尔参数是仍然误导性的,因为你只是在说如果(且仅如果)编译器选择了内置版本的这些运算符,那么编译器将插入对参数的转换。同样,内置(高级)无符号整数加法运算符“+”保证将其参数转换为“unsigned”,但这并不阻止“unsigned int n=-1; n+=3.14;”实际上使用“double”(或者是“float”?)加法运算。(请参考您的“&=”示例。) - Marc van Leeuwen
@MarcvanLeeuwen:抱歉,现在你似乎陷入了无意义的辩护中。但是可能你不知道内置类型的运算符不能被重载。关于内置运算符的保证就是关于内置运算符的保证。 - Cheers and hth. - Alf

3
使用位操作来处理布尔值可以避免因逻辑运算而导致的处理器分支预测逻辑,从而节省了程序开销。将逻辑运算替换为位运算(所有操作数都是布尔类型)可以生成更加高效且功能相同的代码。理想情况下,这种效率应该超过使用逻辑运算时可利用的所有短路优势。
虽然这样可能会使代码难以阅读,但程序员应该注释说明操作原因。

3
与Patrick的回答相反,C ++没有用于执行短路异或的^^运算符。如果你花一秒钟考虑一下,使用^^运算符也没有意义:对于异或,结果总是取决于两个操作数。然而,当比较1 & 2和1 && 2时,Patrick对非bool“布尔”类型的警告同样适用。其中一个经典的例子是Windows GetMessage() 函数,它返回三态BOOL:非零,0或-1。
使用&而不是&&以及|而不是||并不是常见的打字错误,因此,如果您故意这样做,应该注释说明原因。

7
即使考虑了短路运算,一个 ^^ 运算符仍然是有用的。它将 a) 在布尔环境中评估操作数,并且 b) 保证返回 1 或 0。 - Craig McQueen
很简单,只需定义 inline bool XOR(bool a,bool b) { return a!=b; } 就可以得到你想要的结果,但它是一个函数而不是(中缀)运算符。或者你可以直接使用 != 或重载其他具有相同含义的运算符,但当然你需要非常小心,以免意外地使用同名的意外重载。顺便说一下,||&& 返回的是 truefalse,而不是 10 - Marc van Leeuwen
似乎 !||&& 在布尔上下文中对其参数进行求值的原因仅仅是因为这些运算符很少被重载以接受其他类型;据我所知,该语言确实允许这样的重载,因此不保证在布尔上下文中对参数进行求值。 - Marc van Leeuwen

2
帕特里克提出了很好的观点,我不想重复。然而,我建议尽可能使用具有良好命名的布尔变量将“if”语句简化为易读的英语。例如,这是使用布尔运算符,但您也可以使用位运算符并适当命名布尔变量:
bool onlyAIsTrue = (a && !b); // you could use bitwise XOR here
bool onlyBIsTrue = (b && !a); // and not need this second line
if (onlyAIsTrue || onlyBIsTrue)
{
 .. stuff ..
}

你可能认为使用布尔值是不必要的,但它有两个主要作用:
  • 你的代码更容易理解,因为“if”条件中间的布尔值使条件的意图更加明确。
  • 如果你正在使用非标准或意外的代码,比如对布尔值使用位运算符,人们可以更容易地看出你这样做的原因。

编辑:虽然你没有明确表示你想要“if”语句的条件(尽管这似乎最有可能),但我的建议仍然是使用一个中间布尔值。


0
据我所知,许多C++编译器在尝试将位运算的结果转换为布尔值时会发出警告。您需要使用类型转换来使编译器满意。
在if表达式中使用位运算将产生相同的批评,尽管可能不是由编译器发出的。任何非零值都被认为是true,因此类似“if(7 & 3)”这样的语句将为true。这种行为在Perl中可能是可以接受的,但C/C++是非常明确的语言。我认为这是必要的。 :) 我会附加“== 0”或“!= 0”以清楚地说明您的目标。
但无论如何,这听起来像是个人偏好。我会通过lint或类似的工具运行代码,看它是否认为这是一种不明智的策略。就我个人而言,它读起来像是一个编码错误。

你的帖子激励我测试了一下,gcc没有警告!这真是个糟糕的消息,因为我本来想利用那个警告来证明让我运行 &= 来累积布尔结果是可信的,相信其他人如果稍后更改我的测试就会看到警告。但是我的代码会对偶数失败,因为它们会被评估为0/false - 没有警告!现在是重构的时候了... - sage

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