Можно ли использовать побитовые операторы вместо логических?


Побитовые операторы работают с битами, логические операторы оценивают булевы выражения. Если выражения возвращают 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;
}
5 6

5 ответов:

Один из возможных ответов: оптимизация . Например:

if ((age < 0) | (age > 100))
Предположим, что age = -5 нет необходимости оценивать (age > 100), так как выполняется первое условие (-5<0). Однако предыдущий код действительно вычислит выражение (age > 100), которое не является необходимым.

С:

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

Только Первая часть будет оценена.

Примечание: Как @Lundin упоминается в комментариях, иногда | быстрее, чем || из-за точность ветвления для второго варианта (и проблема неправильного предсказания). Поэтому в тех случаях, когда другое выражение настолько дешево, параметр | май будь быстрее. Таким образом, единственный способ узнать в этих случаях-это проверить код на целевой платформе.


Самый важный ответ заключается в том, чтобы избежать неопределенного поведения и ошибок:

Вы можете представить себе этот код:

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

Этот код содержит неопределенное поведение. Однако, если вы замените & на &&, неопределенное поведение больше не существует.

Предположим, что вы получили два значения, которые позволяют вам продолжить, только если они оба ненулевые.

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
}

Существует отчетливая разница между || и |.

В отличие от большинства других операторов языка, логический оператор ||явно определяет порядок вычисления. Первый операнд || должен быть вычислен до второго. Вторую вообще не нужно оценивать.

Это принципиально отличается от |, который ведет себя как большинство операторов: порядок вычисления не определен, и оба выражения будут вычислены. Даже в том случае, когда один операнд оказывается ненулевым, другой операнд все равно будет оцениваться на наличие побочных эффектов.

Означает, что код, подобный f1() || f2(), всегда будет оценивать этот псевдокод:

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

Тогда как f1() | f2() будет выполнять обе функции, в неопределенном порядке, который программист не может знать.

Это также означает, что утверждения типа "|| быстрее, чем |" наивны. Конечно, в случае || второй операнд не обязательно вычисляется, но это происходит за счет ветви, а также ограничения на то, как компилятору разрешается переупорядочивать выражение. Какой оператор, как правило, быстрее, не очевидно.

Даже если вы достигнете того же результата с помощью битового оператора, лучше использовать логический оператор здесь из-за производительности.

В выражении (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>

|| не производит никакого дополнительного ветвления, чтобы иметь возможность пропустить второе вычисление выражения.

Ответ: да, вы можете. Вопрос в том, зачем вам это нужно? Я могу назвать несколько причин, по которым вы не должны:

  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++. Я могу придумать только одну причину для использования побитового оператора для булевых чисел: если вам нужен XOR. Нет оператора ^^, поэтому if (a ^ b) прекрасно, пока оба a и b являются bool и нет стороны задействованные эффекты.