我可以使用位运算符代替逻辑运算符吗?

9

位运算符作用于位,逻辑运算符评估布尔表达式。只要表达式返回bool,为什么不使用位运算符代替逻辑运算符呢?

在这个例子中,我使用位运算符而不是逻辑运算符:

#include <iostream>


int main(){

    int age;
    std::cin >> age;

    if( (age < 0) | (age > 100) ) // eg: -50: 1 | 0 = 1
        std::cout << "Invalid age!" << std::endl;

//  if( (age < 0) || (age > 100) )
//      std::cout << "Invalid age!" << std::endl;

  return 0;
}

7
对于简单的布尔值,可以。我要问你的问题是“为什么”?为了每个操作符节省一个按键而可能导致的错误值得吗? - erip
2
@Alex24 erip 正在问相反的问题。为什么要使用 | 而不是 ||。只是为了节省一个字节吗? - Humam Helfawi
为了反驳你所得到的所有关于为什么不应该这样做的优秀答案,有一种情况是你应该这样做的:密码学。这是一种情况,你积极地想要避免优化。如果在某种情况下回答需要比另一种情况少的时间,这会给攻击者机会去推断为什么你给出了你的答案(这是不好的)。 - Martin Bonner supports Monica
@PeteBecker 我可以看出来你从我的修辞问题中得到了正确的信息。;) - erip
显示剩余2条评论
5个回答

13

一个可能的答案是:优化。例如:

if ((age < 0) | (age > 100))

假设 age = -5,由于第一个条件已经满足 (-5<0),所以无需评估(age > 100)。然而,先前的代码将对不必要的表达式(age > 100)进行评估。

使用:

if ((age < 0) || (age > 100))

第一部分将被评估。

注意:@Lundin在评论中提到的,有时|||更快,这是由于对于第二个选项的分支准确性(以及误预测问题)。因此,在另一个表达式非常简单的情况下,|选项可能会更快。因此,在这些情况下了解目标平台上的代码基准测试是唯一的方法。


最重要的答案是避免未定义的行为和错误:

你可以想象这段代码:

int* int_ptr = nullptr;
if ((int_ptr != nullptr) & (*int_ptr == 5))

这段代码包含未定义行为。但是,如果你用&&替换&,就不会再出现未定义行为。


3
好的回答。在我看来,短路损失是最令人痛苦的。 - StoryTeller - Unslander Monica
6
|| 操作符比较快并不是很明显。"短路"运算意味着 || 操作符必须带有一个分支,而 | 则不需要。因此,认为 || 总是比 | 快是很幼稚的想法。 - Lundin

3
你不应该这样做。假设你收到两个值,只有当它们都非零时才能继续进行。
int b = foo(1); // returns 0x1
int c = foo(2); // returns 0x2

以下条件导致以下结果:b && c == true,而b & c == 0
if (b && c)
{
  // This block will be entered
}

if (b & c)
{
  // This block won't
}

2
隐式地,您正在检查 b != 0 && c != 0,因此如果您编写了长格式版本,它仍将起作用。仍然是一个很好的答案,大多数人(包括我自己)会像您一样编写它。 - erip
@StoryTeller,你应该比较 b!=0 & c!=0,而不是 b&c - Pacerier

2

||| 有明显的区别。

与语言中的大多数其他运算符不同,逻辑运算符 || 明确指定了评估顺序。必须先评估 || 的第一个操作数,而第二个操作数则不一定需要评估。

这与 | 基本不同,后者的行为类似于大多数运算符:评估顺序是未指定的,两个表达式都将被评估。即使在发现一个操作数为非零时,仍将评估另一个操作数以进行副作用。

也就是说,像 f1() || f2() 这样的代码总是会评估为这个伪代码:

if(f1() != 0)
{
  f2();
}

f1() | f2()会执行两个函数,但程序员无法知道执行的顺序。

这也意味着像"||比|更快"这样的语句是幼稚的。当然,在||的情况下,第二个运算数不一定被求值,但这是以分支的代价为代价的,并且限制了编译器允许重新排序表达式的方式。哪个运算符通常更快并不明显。


1
“未指定顺序”的引用需要注明出处。 - Pacerier
@Pacerier 所有运算符都遵循C17 6.5/3中的规则,除非另有明确说明:“除非另有规定,子表达式的副作用和值计算是无序的。” 这部分在早期版本的语言中更易读,但意义完全相同。从C99 6.5/3开始:“除了特别指定(对于函数调用(),&&,||,?:和逗号运算符),子表达式的求值顺序和副作用发生的顺序都是未指定的。” - Lundin
@Pacerier 一个C程序员应该无需阅读标准引用即可了解这一点。评估顺序是语言的重要组成部分。不幸的是,初学者课程和书籍很少涉及此主题,而是过于关注运算符优先级。 - Lundin

1
即使使用位运算符可以达到相同的结果,由于性能原因,在此处最好使用逻辑运算符。
在表达式 (age < 0) || (age > 100) 中,只有在第二个条件 (age > 100) 的前提条件 (age < 0)false时才会计算。对于这样的表达式,编译器会生成以下代码:
cmpl   $0x0,-0x4(%rbp)                 
js     1004010f9 <main+0x19>  // <-- Skip right expr evaluation if left true
cmpl   $0x64,-0x4(%rbp)               
jle    100401100 <main+0x20>

||不会产生额外的分支,以便跳过第二个表达式的评估。


“||”更快并不明显。 “短路”评估意味着“||”运算符必须与分支一起使用,而不像“|”。 - Lundin
@Lundin 您是正确的。但就表达式左右两侧的评估而言,它更快。我并不是说它总是更快。| 可能会给出更好的性能结果,但原始表达式应该以较少数量的 < 操作符形式重写。 - Nikita

0

答案是肯定的,你可以这样做。问题是为什么要这样做呢?我可以列举一些你不应该这样做的原因:

  1. 这会让其他程序员非常困惑。
  2. 很容易忽略其中一个操作数不是 bool 类型,这可能导致微妙的错误。
  3. 操作数的求值顺序是未定义的。
  4. 它会破坏短路规则。

为了说明最后一点:

bool f1() {
    cout << "f1" << endl;
    return true;
}

bool f2() {
    cout << "f2" << endl;
    return true;
}

int main() {
    if (f1() || f2()) {
        cout << "That was for ||" << endl;
    }
    if (f1() | f2()) {
        cout << "That was for |" << endl;
    }
    return 0;
}

它打印:

f1
That was for ||
f1
f2
That was for |

假设f2可能具有重大的副作用(if (okToDestroyWorld && destroyWorld()) ...),那么差异可能是巨大的。这对于Java程序员来说并不奇怪(其中|||实际上由语言规范定义为布尔值),但在C++中却不是常见做法。
我只能想到一种使用位运算符处理布尔值的原因:如果需要使用异或操作。没有^^运算符,因此if (a ^ b)可以正常工作,只要ab都是bool类型且没有涉及任何副作用。

1
int destroyWorld() { printf("Goodbye, World...\n"); return destroyWorld(); } - erip
@erip,返回类型是错误的。但既然它从未返回,我想这没关系。 - Sergei Tachenov
哇塞。:) 更快地喝咖啡 - erip
答案是肯定的,您可以。但我有疑问,如果代码是这样的:if ((int_ptr!=nullptr) & (*int_ptr==5))呢? - Humam Helfawi
@Humam,你可以做到并不意味着它是同一件事。我的第三点用短路作为例子阐明了同样的问题。 - Sergei Tachenov

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