2.11. Supplemental: Bitwise Operators

Some exotic programming sub-disciplines like communications, embedded programming, and cryptography use bitwise operations extensively. Although they are used less frequently in "ordinary" programming, they are necessary for common tasks like file I/O operations, which the text covers in its last chapter. C++ implements bitwise operations with operators, making their inclusion in this chapter appropriate. However, the text doesn't rely on them significantly until it covers file I/O, so it labels them as "supplemental" until revisiting them later.

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 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 operands as a short string of bits for 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, illustrating the meaning of "the bitwise operators operate on the corresponding bits of the two operands." Operating on two bits with a bitwise operator produces 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 switch 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 switch 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 reversible: if A^B=C, then A^C=B, and C^B=A.
a ~a
0 1
1 0

~12 = 3

~ 1100
------
  0011
Bitwise Complement. The bitwise complement operator calculates the one's-complement by toggling the 1s to 0s and the 0s to 1s. Adding 1 to the one's complement produces the two's complement.

Viewing the bitwise operations in base-10 is potentially confusing. Nevertheless, they are useful in specialized situations, especially when one operand is a constant. Unfortunately, C++ doesn't have a binary number notation - and long strings of 1s and 0s are problematic. Therefore, programmers typically denote integer constants treated as binary constants with hexadecimal (or occasionally in octal) numbers. A single hexadecimal digit corresponds to a nibble (i.e., to 4 bits). So, we can compactly specify each 4-bit cluster as a single hexadecimal digit.

Decimal (base-10) Octal (base-8) Hexadecimal (base-16) Binary (base-2)
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 0111
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. We can form bit patterns with numerical values in any base, but hexadecimal is particularly convenient, especially for longer patterns. C++ directly supports decimal, octal, and hexadecimal notation. You can recreate this table by beginning each column at zero and counting in the appropriate base, remembering to carry. Decimal numbers are undecorated, while octal and hexadecimal numbers begin with a leading 0 and x, respectively.

Bitmasks and Bit Vectors

It's convenient to think of bits as switches: a 1 represents when the switch is on and 0 when it is off. Programmers typically use the logical-AND and logical-OR operators to set (switch on), reset (switch off), and test the bits stored in a multi-bit data structure. Programmers can implement the structures in many ways, but the easiest approach - the one illustrated here - is using the individual bits in an integer. We use the term bit-vector to distinguish an integer representing an integer used as a bit container from one used as a number. The names bit-fields, bit-sets, bit-maps, and bit-strings are synonyms. A constant bit-vector, often symbolically named for convenience, is called a bitmask. Programs often use them to represent common or frequently used switch settings.

Bitwise-AND
a b a & b
0 0 0
0 1 0
1 0 0
1 1 1
  1011 0110 = 0xb6
& 0001 1111 = 0x1f
------------------
  0001 0110 = 0x16
An image depicting bit-masks as a grate through which each bit must pass. Bits form the slots in the grate. For bitwise AND, 1s represent open slots in the grate that allow the bits to pass through unmodified, while 0s switch bits off, always outputting a 0 regardless of the input.
(a)(b)(c)
Masks and bitwise-AND. Programmers turn switches or settings off with the Bitwise-AND operator
  1. Both operands must be a 1 to produce a 1; any other combination produces a 0.
  2. The bitwise-AND operator, &, masks out (switches off) some bits in a bit-vector. The operators process the bits in corresponding positions in each operand (the same column in the illustration).
  3. Imagine the bit-mask as a filter: 0s close the filter, blocking out the corresponding data bits, while 1s are open, passing the corresponding data bits through.
Bitwise-OR
a b a | b
0 0 0
0 1 1
1 0 1
1 1 1
  1011 0110 = 0xb6
| 1110 0000 = 0xe0
------------------
  1111 0110 = 0xf6
An image depicting bit-masks as a grate through which each bit must pass. Bits form the slots in the grate. For bitwise OR, 0s represent open slots in the grate that allow the bits to pass through unmodified, while 1s always output a 1 regardless of the input value.
(a)(b)(c)
Masks and bitwise-OR. Programmers turn switches or settings on with the bitwise-OR operator.
  1. Both operands must be 0 to produce a 0; any other combination produces a 1.
  2. The bitwise-OR operator, |, switches on some bits in a bit-vector.
  3. 0s in the bit-mask pass through the corresponding data bits without changing them. 1s in the mask inject 1s in the result's corresponding bit position or column.

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 1s and 0s and shift them 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. Two views of the left shift operator. The shift operation moves an integer's bits to the left. The operation discards the most significant bits, illustrated with strikeout characters, and opens spaces in the least significant positions.

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 operation shifts the bits out on the right side, discarding them as expected. However, how the operation fills the empty spaces on the left complicates the right shift operator.

Without programmer intervention, the underlying hardware 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 with a copy of the left-most bit), and some hardware does not (i.e., it fills the empty spaces with 0s).

Fortunately, programmers can intervene. In a signed integer (a number capable of storing negative and positive values), the highest-order bit is called the sign bit. Computers generally treat a number as negative when the sign bit is 1 and non-negative (i.e., zero or positive) when it is 0. Negative values are generally not needed when dealing with bit patterns, and so the easy "fix" is defining the integer as unsigned. (Using unsigned integers, variables, and constants with all the bitwise operators is common.) When the right shift operator's left operand is unsigned, it always fills the empty spaces on the left with 0s 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. Two views of the right shift operator without sign extension. The shift operation moves an integer's bits to the right. It discards the least significant bits, illustrated with strikeout characters, and opens spaces in the most significant positions. Three cases produce the illustrated results: (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 extension. Two views of the right shift operator with sign extension. The shift operation moves an integer's bits to the right. The shift discards the least significant bits, illustrated with strikeout characters, and opens spaces in the most significant positions. However, how the computer fills the open spaces depends on a combination of circumstances. First, sign extension is a property of the underlying hardware and beyond program control. sign-extend hardware copies the highest-order bit into the opened positions. So, the second circumstance is the value of the highest-order bit, highlighted with orange, when the shift operation occurs. So, the program produces the illustrated result only when the left operand is signed, the hardware performs sign extension, and the highest-order bit is 1.

Bitwise Operators With Assignment

Earlier in the chapter, we saw that C++ allows a shorthand notation with arithmetic operators called "Operation With Assignment." We can also use this notation with the binary bitwise operators.

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).