I've seen a few posts here 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.
Top comments (2)
Hi, Bosley
I write a blog post about bitwise operation of C/C++ recently. I think you may be interested for it so I want to share with you. Here is the link, Convert the endianness in C++ and test it with GDB and Python
Best regards, Gapry.
Thank you for sharing!