This is a post that I wrote on dev.to regarding bitwise operations

I've seen a few posts here (dev.to) and there talking about bitwise operations. I enjoyed reading them, but a lot of the best ones (imo) didn't show great examples of bitwise usage. As someone who leverages bitwise operations everyday at my job, I figured I'd make a post to potentially offer some clarity on the subject by writing up a big example.

Before we get to examples of where we might use this stuff, lets look over the bitwise operations.

If you'd like to see executable code with all of this information, check it out here.

AND

AND-ing two binary values will indicate when corresponding bits in the two inputs are '1' or in the 'on' position.

For example :

    00000101 & 00000011 = 00000001

Here is the same thing, but stacked to give an easier visual representation of what is going on:

    00000101
    00000011
    --------
    00000001

OR

OR-ing two binary values will yield a result that tells us if a bit in one input OR a bit in the other input is '1' or in the 'on' position.

For example :

    01001000 | 10111000 = 11111000

Here is the same thing, but stacked to give an easier visual representation of what is going on:

    01001000
    10111000
    --------
    11111000

XOR

XOR-ing is pretty cool. Similar to OR, but different in that it requires exclusivity in the input bits being in the '1' or 'on' position. Easily understood as "One or the other, but not both."

    11111111 ^ 00001111 = 11110000

Here is the same thing, but stacked to give an easier visual representation of what is going on:

    11111111
    00001111
    --------
    11110000

NEG

Negation! This can be understood as 'flipping' or 'toggling' the bits.

    ~11111010 = 00000101

Again, the stacked example:

    11111010
    --------
    00000101

Shifting

The last thing we will look at before the big example is shifting. Shifting is neat. We basically just push the bits left or right within the binary number.

    0x01      = 00000001
    0x01 << 1 = 00000010
    0x01 << 2 = 00000100
    0x01 << 3 = 00001000

Of course, we can also shift in the other direction!

10000000 >> 5 = 00000100

As you can see, by >> by '5' we've moved the bit 5 spaces to the right. Wicked!

Time for some use-cases (examples)!

These are some cases where we might chose to use bitwise logic.

MSB Checking

This one might seem strange, but there are cases where we might want to see if the far-left bit of a byte is in the 'on' position.


void someFunc() {

    uint8_t var = 0x8A; // In binary, this is : 10001010

    // If we want to see if the most significant bit is set 
    // we can right shift a few places and use it as a bool
    // because 1's are true, and 0's are false. 

    bool isBitSet = var >> 7;

    if( isBitSet ) {
        // Of course, we COULD drop ' var >> 7 ' in as the conditional
        // directly, but for the example I decided not to.

        std::cout << "The bit is set!" << std::endl;
    }

}

Masking

Lets say we have a 32-bit number that represents a color! We all like colors. Why 32-bit ? Because it can happen. Lets say our color happens to be encoded as-follows:

    Alpha = Byte One
    Blue  = Byte Two
    Green = Byte Three
    Red   = Byte Four

Given the following 32-bit binary number, how can we extract these values?

    Alpha    Blue     Green    Red
    10001100 11101001 00001010 00000011

MASKS

We'll use the following masks:

    uint32_t alphaMask = 0xFF000000; // Mask to obtain alpha byte
    uint32_t blueMask  = 0x00FF0000; // Mask to obtain blue byte
    uint32_t greenmask = 0x0000FF00; // Mask to obtain green byte
    uint32_t redMask   = 0x000000FF; // Mask to obtain red byte

Using these masks, we can leverage the functionality of AND to ensure that we get the value we want... and that is pretty great.

    //                     A B G R
    uint32_t abgrColor = 0x8CE90A03;    // The actual 32-bit color

    uint32_t alpha = abgrColor & alphaMask;
    uint32_t blue  = abgrColor & blueMask;
    uint32_t green = abgrColor & greenmask;
    uint32_t red   = abgrColor & redMask;
}

To demonstrate one of these as we did above, lets take the binary representations of abgr and stack it on the blue mask.

10001100 11101001 00001010 00000011
00000000 11111111 00000000 00000000
-----------------------------------
00000000 11101001 00000000 00000000

Hooray! We've discovered that the byte representing our blue color is :

    11101001

Chopping!

Lets say we have 32 bits ( a color maybe? ) and we want to send it somewhere. The specification for the protocol that we need to send it over demands we do it 8 bits at a time. Why? I don't know, I didn't write the spec.

In order to do this we need to chop the 32 bits up into 4 bytes and send them sequentially. Then, on the other side, we need to reconstruct the original data.

Its okay, we can do this.


    uint32_t var = 0x8CE90A03;           // This is our color, but in hex 

    uint8_t pack[4];                     // A 'pack' of 4 bytes

    // Using a mask (like above) we grab the data 
    pack[0] = ( var  & 0x000000FF);
    pack[1] = ( var  & 0x0000FF00) >> 8;  // But when we mask some bits we 
    pack[2] = ( var  & 0x00FF0000) >> 16; // need to move them into a range
    pack[3] = ( var  & 0xFF000000) >> 24; // that works within a byte

Explanation

If the masking and shifting seems confusing maybe this will help!
If you recall from the blue byte extraction above we ended up with the following data:

    Byte 1   Byte 2   Byte 3   Byte 4
    00000000 11101001 00000000 00000000

However, uint8_t represents 1 byte. If we attempted to construct a uint8_t with that data, it would be 00000000 as only 'Byte 4' would be used. That means we need to get JUST the data in 'Byte 2' we have to right shift by 16.

      00000000 11101001 00000000 00000000 >> 16 
      -----------------------------------
      00000000 00000000 00000000 11101001

Woot! Now that '11101001' is in the far right we can safely construct a uint8_t and preserve the 'blue' data.

/Explanation

Now that we have a pack of bytes representing our color data, lets just assume we called something to send it, and now we need to reconstruct it as a uint32_t on the receiver end.

This is relatively easy, but not necessarily straight forward.

uint32_t unpacked = pack[0] | (pack[1] << 8) | (pack[2] << 16) | (pack[3] << 24);

We're done!

But what did we do?

We undid all of the chopping of course! We took each byte, and placed them into their corresponding space within the new 32-bit number.

Lets take it step by step.

When we packed the data, masked each byte and put it in the pack. Here is what it looked like:

    initial data = 10001100 11101001 00001010 00000011

    pack[0] = 00000011  // red
    pack[1] = 00001010  // green
    pack[2] = 11101001  // blue
    pack[3] = 10001100  // alpha

As we reassembled the bytes, we followed these steps:

    // Created a 32-bit variable
    00000000 00000000 00000000 00000000

    // Added pack[0]
    00000000 00000000 00000000 00000011

    // Added pack[1] (by or-ing), and shifted it left 8 bits
    00000000 00000000 00001010 00000011

    // Added pack[2] (by or-ing), and shifted it left 16 bits
    00000000 11101001 00001010 00000011

    // Added pack[3] (by or-ing), and shifted it left 24 bits
    10001100 11101001 00001010 00000011

Did it work? Lets check

    Original value: 10001100 11101001 00001010 00000011
    Reconstructed : 10001100 11101001 00001010 00000011

It sure looks like it worked!

Ending remarks

That was a lot of words for me. I don't usually write things for people, but this bitwise stuff is pretty useful and I enjoy it quite a bit.

If you've made it this far and haven't done so yet, you should check out the code I wrote to demonstrate everything.

This was my first article here, I hope I made things clear and didn't goof anything up. If you noticed any mistakes, or if anything is unclear please let me know and I will do my best to clarify things and/or fix them.