2.11. Supplemental: Bitwise Operators

Courses preparing students for specialized topics like embedded programming or writing device drivers often cover the bitwise operators in detail, but introductory C++ courses generally do not. Furthermore, providing realistic examples also involves programming structures not introduced until the next chapter. Still, we briefly cover bitwise operators here for completeness and provide a consistent location for later reference and review.

Unless your instructor explicitly assigns this section, you may skip it for now and return later when these operations are more relevant.

C++ inherits six bitwise operators from C. Bitwise operations are always available in assembly language but are less common in higher-level languages. Including these operators in C made it possible to write operating systems and device drivers in C rather than in assembly. Most bitwise operators require two integer arguments, but complement is a unary operator. Three operators act on the corresponding bits of the two operands; we can conveniently summarize these and the complement operators with truth tables. Two operators treat one operand as a string of bits and shift them to the left or the right. We'll often view one or both the operands as a short string of bits for our convenience and ease of illustration.

Basic Bitwise Operators

The basic bitwise operators are simple enough to describe them with simple truth tables. When we use these operators, it's convenient to think about or view both operands in binary: 1s and 0s. Each 0-value corresponds to false and each 1-value corresponds to true. A simple example follows each truth table, which illustrates the meaning of "the bitwise operators operate on the corresponding bits of the two operands." The result of combining two bits with a bitwise operator is a single bit. The following figures detail

a b a & b
0 0 0
0 1 0
1 0 0
1 1 1

12 & 9 = 8

  1100
& 1001
------
  1000
Bitwise-AND. Both operands must be 1 to produce a 1. Bitwise-AND is used to turn off or mask out bits.
a b a | b
0 0 0
0 1 1
1 0 1
1 1 1

12 | 9 = 13

  1100
| 1001
------
  1101
Bitwise-OR. Both operands must be 0 to produce a 0. Bitwise-OR is used to turn on or set a bit to 1.
a b a ^ b
0 0 0
0 1 1
1 0 1
1 1 0

12 ^ 9 = 5

  1100
^ 1001
------
  0101
Bitwise XOR (Exclusive-OR). Operands must be different to produce a 1. XOR is a reversible operation: if A^B=C, then A^C=B, and C^B=A.
a ~a
0 1
1 0

~12 = 3

~ 1100
------
  0011
Bitwise Complement. Toggles 1's to 0's and 0's to 1's. The result is called the one's complement. If you add one to the one's complement, you get two's complement.

Viewed in base-10 (below each truth table in the figures above), the bitwise operations don't seem to make much sense. Nevertheless, they are useful in specialized processes, especially when one operand is a constant, often called a bitmask. C++ does not have a notation for representing binary constants, so integer constants are typically denoted in hexadecimal (or occasionally in octal). A single hexadecimal digit corresponds to a nibble (i.e., to 4-bits). So, we can compactly specify each 4-bits of a constant as a single hexadecimal digit.

Decimal Octal Hexadecimal Binary
0 0 0x0 0000
1 01 0x1 0001
2 02 0x2 0010
3 03 0x3 0011
4 04 0x4 0100
5 05 0x5 0101
6 06 0x6 0110
7 07 0x7 0110
8 010 0x8 1000
9 011 0x9 1001
10 012 0xa 1010
11 013 0xb 1011
12 014 0xc 1100
13 015 0xd 1101
14 016 0xe 1110
15 017 0xf 1111
Forming bit patterns. Bit patterns can be represented by numerical values in any base, but hexadecimal is particularly convenient, especially for longer patterns.

Bitmasks

One of the most common uses for bitwise operations is to set, reset, and test bits stored in a multi-bit data structure. A "multi-bit data structure" may be as simple as an integer, which may hold anywhere from 8- to 64-bits, or an array (chapter 7) of integers. Multi-bit data are often called bit fields, bit sets, bit vectors, bit maps, bit strings, etc.

Bit fields are often used to represent a wide variety of settings or conditions compactly. Each bit acts as a switch. A 1 indicates that a setting is on, while a 0 indicates that the setting is off. Programs often use bitwise-OR and bitwise-AND operators in conjunction with constant bit fields, called bitmasks, to switch bits on and off respectively, which corresponds to switching settings on or off.

An image depicting bitmasks as a grate through which each bit must pass. Slots in the grate are formed by 0's or 1'. For bitwise-AND, 1's represent open slots in the grate that allow the bits to pass through unmodified, while 0's switch bits off, always outputing a 0 regardless of the input.
Bitwise-AND is used to turn switches or settings off. A "0" in the bitmask. forces the corresponding bit in the result to a be "0": 0 & x = 0 (where x may be either "0" or "1"). "1's" appearing in the mask allow the original bits to "pass through" unmodified: 1 & x = x.
An image depicting bitmasks as a grate through which each bit must pass. Slots in the grate are formed by 0's or 1'. For bitwise-OR, 0's represent open slots in the grate that allow the bits to pass through unmodified, while 1's always output a 1 regardless of the input value.
Bitwise-OR is used to turn switches or settings on. A "1" in the bitmask. forces the corresponding bit in the result to a be "1": 1 | x = 1 (where x may be either "0" or "1"). "0's" appearing in the mask allow the original bits to "pass through" unmodified: 0 | x = x.

Input and output operations in a C++ program generally takes place through stream objects (e.g., cin and cout). Programmers control the behaviors of these objects with a set of 1-bit switches or flags. Each bit corresponds to a specific behavior - setting the bit to 0 disables the behavior, and setting it to 1 enables it. Programmers use named bitmasks (i.e., constant values) and bitwise operators to bit settings. Four of the file I/O bitmasks illustrate the concept:

in
Open the file for input (i.e., the program can write to the file)
in = 0x01 = 00000000000000000000000000000001
out
Open the file for output (i.e., the program can read from the file)
out = 0x02 = 00000000000000000000000000000010
app
Append new data to the end of the file (i.e., preserve existing data in the file)
app = 0x08 = 00000000000000000000000000001000
binary
Threat the file contents as binary data (e.g., .gif, .jpg, .wav, .mp3, etc.) files
binary = 0x20 = 00000000000000000000000000100000

To create a value that will switch on all four behaviors, a programmer might write an assignment statement such as:

int mode = in | out | app | binary;

The bitwise-OR operations produce the bit field: 00000000000000000000000000101011, which the programmer could then use to open a file that has the combined behaviors.

The library code that actually opens the file may look similar to the following:

if (mode & binary != 0)
{
	// then configure the file for reading or writing in binary mode
}
if (mode & app != 0)
{
	// then configure the file for writing in append mode
}
						. . . .

The bitwise-AND operation switches off all of the bits except the one corresponding to the binary bitmask. The magnitude (i.e., the exact value) of mode & binary is not important; all that is important is that the value is not 0. If "binary" had been left out of the assignment statement above that calculated "mode," then the expression mode & binary would produce a 0 and the code for configuring the file for binary I/O would be skipped.

Bit-Shift Operators

The two bit-shift operators should look familiar to you, not because we have used them before, but because they are reused as the output and input operators introduced previously. Both operands are integers, and we will continue to view the left-hand operand in binary but will now view the right-hand operand in decimal. Both bit-shift operators treat the left-hand operand as a string of 1's and 0's that are shifted either left or right by the number of places indicated by the right-hand operand. Shifting may seem confusing but is easy to understand when illustrated with an example.

Shifting Left

The left shift operator, << moves the bits in an integer to the left. The right-hand operand specifies how many places to shift the bits. For example:

11001100 (base 2) << 2 (base 10) is 00110000
Left Shift Operator.

Shifting Right

The right shift operator, >> is similar to the left shift operator but is a little more complicated. The right shift operator moves the bits in the left-hand operand to the right by the number of places specified by the right-hand operand. The bits shifted out on the right side are discarded as expected. But it is how the operation fills the empty spaces on the left that makes the right shift operator complicated.

Without programmer intervention it is the underlying hardware that determines how the spaces vacated by the shift are filled. (The ANSI standard calls such features implementation dependent.) Some hardware implements sign extension (i.e., it fills the empty spaces a copy of the left most bit), and some hardware does not (i.e., it fills the empty spaces with 0's).

Fortunately, programmers can intervene. The highest-order bit in a signed integer, the sign bit, denotes a negative value when it is 1 and a non-negative value when it is 0. A negative value is generally not needed when an integer stores a bit pattern, and so the easy "fix" is to define the integer as unsigned. (It's quite common to use unsigned integers, variables, and constants with all of the bitwise operators.) When the right shift operator's left operand is unsigned, then the empty spaces on the left are always filled with 0's regardless of how the hardware behaves by default.

The following examples demonstrate the right shift operator with and without sign extension:

11001100 (base 2) >> 2 (base 10) is 00110011
Right Shift Operator. This result is seen in three situations: (a) the left operand is unsigned, (b) the hardware does not perform sign extension, or (c) the original highest-order bit is a 0.
11001100 (base 2) >> 2 (base 10) is 11110011
Right Shift Operator with sign extend. Right shift with sign extend has two possible results that depend on the value of the original, highest-order (i.e., the left-most or the most significant) bit. If the highest-order bit is a 1 (as highlighted in orange), then the 1 bit is copied into each vacated or opened bit position, as illustrated in Figure 10. If the highest-order bit is a 0, the operation copies the 0 into the vacated bit position, and the final result looks like Figure 9. The operation only produces the result illustrated in Figure 10 when all three of the following conditions are true: the left operand is signed, the hardware performs sign extension, and the highest-order bit before shifting is a 1.

Bitwise Operators With Assignment

Earlier in the chapter, we saw that C++ allows a shorthand notation with arithmetic operators called "Operation With Assignment." The operation with assignment notation may also be used with all the binary bitwise operators as well.

Operation With Assignment Meaning
V &= EV = V & E
V |= EV = V | E
V ^= EV = V ^ E
V >>= IV = V >> I
V <<= IV = V << I
Operation with assignment with bitwise operators. B is a Boolean variable, E is a Boolean-valued expression, and I is an integer-valued expression (often a constant or variable).