About &= and |=

I often happen to see, on pretty old projects, the operators &= and |= used to update a boolean indicating success that is returned along successive function calls. As in the following example :

bool success = true;
success &= DoOneThing();
success &= DoOneOtherThing();
success &= DoALastThing();
if (success)
    std::cout << "Success !" << std::endl;

This way of writting is more concise than the usual :

bool success = true;
success = success && DoOneThing();
success = success && DoOneOtherThing();
success = success && DoALastThing();
if (success)
    std::cout << "Success !" << std::endl;

But you should be cautious, because behind the operators &= and |= hide a few subtelties that one should be aware of.

Definition of the operators &= and |=

First, let’s see precise definition of &= and |=. In this section, I’ll only use &= as an example, but the behavious of |= is pretty much the same.

&= is an arithmetic binary operator, which permforms a binary AND and its assignation. In other terms, writting this :

v1 &= v2;

is equivalent to this :

v1 = v1 & v2;

except that v1 is only evaluated once.

For more insight about this, read the C++11 standard section 5.17 Assignment and compound assignment operators or go to https://en.cppreference.com/w/cpp/language/operator_arithmetic

What we call “arithmetic binary operators” (which use the symbols &, |, ^, << and >>) are operators that perform bit-to-bit operations.

For instance :

int a = 0b110;
int b = 0b011;
int c = a & b; // AND - 010
int d = a | b; // OR  - 111
int e = a ^ b; // XOR - 101

Writting a &= b; is thus performing a bit-to-bit AND operation on a and b, and assigns the result to a.

Differences between &= and &&, |= and ||

The operators && and || are, as for them, logical operators, as opposed to &= and |= who are arithmetic operators.

The use of &= and |= in a logical context is prone to diverge from what we could expect them to do.

Here are two example to illustrate that.

Example 1

#include <iostream>

bool foo()
{
    std::cout << "call foo()" << std::endl;
    return true;
}

int main() {
    bool c = true;
    std::cout << "Using |=" << std::endl;
    c |= foo();
    std::cout << "Using = and ||" << std::endl;
    c = c || foo();
    std::cout << "End" << std::endl;
    return 0;
}

The function foo() always returns true, as long as writing on the standard output to indicate it’s been called.

In the main() function, we create a bool c, initialized true, on which we will perform the arithmetic binary operator |= and the logical operator ||.

Here is the standard output :

Using |=
call foo()
Using = and ||
End

We can plainly see that in the |= case, foo() is called whereas it is not in the || case.

This is simply due to the fact that, in C++, logical operators are lazy. This means that if the right-hand-side of the operation can not change its result after evaluating the left-hand-side, then it won’t be evaluated.

Here, because c is already true in the expression c || foo(), no matter the value of foo() the result of the operation will always be true.

In the case of |=, it is a bit different. |= is not a logical operator, it does not reason in terms of true or false, but it performs arithmetic operations on integers. Thus, it needs to evaluate each side of the operation before evaluating the result. With arithmetic operations, both side are always evaluated.

Whether you wish the right-hand-side to be evaluated depends on the context. However, keep in mind that when C++ developpers see logical-like operations, they expect it to be lazy.

Example 2

The following example, though quite uncommon in practice, can lead to errors that are difficult to investigate.

#include <iostream>

int main() {
    int c1 = true;
    int c2 = 0b10;

    if (c1)
        std::cout << "c1 is true" <<std::endl;
    else
        std::cout << "c1 is false" <<std::endl;

    if (c2)
        std::cout << "c2 is true" <<std::endl;
    else
        std::cout << "c2 is false" <<std::endl;

    if (c1&&c2)
        std::cout << "c1&&c2 is true" <<std::endl;
    else
        std::cout << "c1&&c2 is false" <<std::endl;

    c1 &= c2;

    if (c1)
        std::cout << "c1&=c2 -> c1 is true" <<std::endl;
    else
        std::cout << "c1&=c2 -> c1 is false" <<std::endl;

    return 0;
}

Here, two variables are created : c1 which is initialized to true, and c2, which is initialized to 0b10.

Following four blocs performing logical evaluations :

  • First we logically evaluate c1.
  • Then, we similarly evaluate c2.
  • Then we evaluate the logical operation c1 && c2.
  • Finaly, we realise the arithmetic binary operation c1 &= c2 just before evaluating c1 again.

Here is the standard output of this code :

c1 is true
c2 is true
c1&&c2 is true
c1&=c2 -> c1 is false

Let’s see in details how this works :

First of all, c1 is considered true. This is expected because it is explicitly initiliazed with true.

Secondly, c2 is also considered true. This is so because, in C++, any non-null integer evaluates to true.

Thirdly, c1 && c2 is evaluated to true. Indeed, because c1 and c2 and individually true, performing the AND operation on them is logically true.

Fourthly, we perform c1 &= c2;. By doing so, we actually perform c1 & c2 and affect the result to c1.

However, according to the standard, true is equal to 1 when promoted to integer. Thus, c1 & c2 is equivalent to 0b01 & 0b10. The result to this operation really is 0b00, the bit-to-bit AND operation is false for every bit.

The 0 value being evaluated to false in a logical expression, c1 is, after all, false.

You won’t see this happen very often in live code, but there is plenty of situations, especially in legacy code, when an integer value that is not 1 is returned as if it was a true boolean.

Conclusion

The use of &= and |= will do what we expect in 99% of the case. But in the remaining 1%, you’ll have major bugs difficult to investigate.

Moreover, I’d like to pinpoint a semantic problem here. The fact that we ue an arithmetic operation instead of a logical one mislead the reader and forces them to do an additional effort to pull this straight. In addition to that, it may prevent the compiler to perform optimization.

Writting a = a && b; instead of a &= b; is not much an effort, with only a few additional characters, but it will protect you from bugs and hold better semantics.

Thank you for your attention and meeting you next week !

NB : The example were done with clang and the compiler options --std=c++20 -O3

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s