The bitwise shift operators << and >> move every bit in their left operand a fixed number of positions. They are most often encountered in C++ as overloads for stream I/O, however they are also overloaded to perform bit manipulation. This has widespread use in embedded systems.
They have 2 main strengths:
- They are the fastest way to multiply or divide by powers of two
- They are the standard tool for assembling and disassembling multi-byte values.
The Basics
Left shifting shifts the bits n positions of the left, towards MSB.
Right shifting shifts the bits n positions to the right, towards LSB.
Neither operator modifies its operand:
#include <bitset>
#include <iostream>
int main()
{
std::bitset<4> bits{ 0b0001 };
std::cout << "Original: " << bits << '\\n';
std::cout << "Shift by 1: " << (bits << 1) << '\\n';
std::cout << "Shift by 2: " << (bits << 2) << '\\n';
std::cout << "After both: " << bits << '\\n';
}
// Output
Original: 0001
Shift by 1: 0010
Shift by 2: 0100
After both: 0001
Each bits << n produces a new value; bits itself is unchanged. This explains why the last line prints 0100.
To shift in place (thereby permanently modifying the operand), use the bitwise assignment operators bits <<= 2.
NOTE: Bit-shifting in C++ is endian-agnostic. Left-shift is always towards the most significant bit, and right-shift towards the least significant bit.*
Performing mathematical operations via bit-shifting
Because binary is base-2, moving a bit one position to the left doubles its positional value. Thus, we can bit shift as a shortcut to performing multiplication and division.
0001 = 1
0010 = 2
0100 = 4
1000 = 8
// Mathematically, shifting left by n positions is equivalent to multiplying by 2^n.
3 << 1 // 3 * 2^1 = 6
3 << 2 // 3 * 2^2 = 12
3 << 3 // 3 * 2^3 = 24
// Similarly, shifting right by n positions is equivalent to integer division by 2^n.
24 >> 1 // 24 / 2^1 = 12
24 >> 2 // 24 / 2^2 = 6
24 >> 3 // 24 / 2^3 = 3
In embedded firmware, these shortcuts can matter. A bare-metal microcontroller without a hardware multiplier will execute x << 3 in a single cycle but x * 8 as a multi-cycle library call.
A real-life example
Sensors and peripherals routinely return multi-byte readings split across separate registers. A fuel gauge communicating over I²C, for example, might place a 16-bit voltage reading into a two-byte buffer, low byte first:
uint8_t buffer[2]{}; // [0] = low byte, [1] = high byte
uint16_t voltage_mV = (static_cast<uint16_t>(buffer[1]) << 8) | buffer[0];
The high byte is shifted left by eight positions to occupy bits 8–15 of the result, and the low byte is OR-ed in to fill bits 0–7. The final value is a single 16-bit reading reconstructed from two registers.
The static_cast<uint16_t> is not for looks. buffer[1] << 8 promotes uint8_t to int before shifting.
Another Example: Direct Digital Synthesis (DDS)
Direct Digital Synthesis (DDS) is a technique for generating analogue waveforms from a digital source. The premise is:
- Numerically step through a waveform that is stored in a lookup table (LUT)
- Convert those numbers into analog voltage (through PWM or a DAC)
- The result is a continuous analog signal.
A common DDS design uses a phase accumulator, which is basically an integer counter.
While the accumulator might be 16-bit or 32-bit for precision, your Waveform Lookup Table (LUT) is usually much smaller (e.g., 256 entries, or 8-bit) because embedded RAM is precious.
Thus, a need arises for bit-shifting.
In order to index the wavetable, we must convert a 16 or 32-bit value into a 8-bit value.
uint16_t phaseAccumulator {};
uint8_t waveTable[256] {}; // 8-bit lookup table (2^8 = 256)
// Our index requires an 8-bit value
// Right shifting discards the lower byte
uint8_t index = phaseAccumulator >> 8; // Equivalent to phaseAccumulat / 256
// The lower byte is discarded
uint8_t sample = waveTable[index];
In Conclusion
Bit shifting allows us to bridge the two worlds of hardware and software.
It is the lowest-friction way to multiply and divide by powers of two, and the standard idiom for assembling multi-byte values from hardware registers. Reach for it when the problem is genuinely about bits or byte layout — register reconstruction, fixed-point scaling, table indexing — and use the regular arithmetic operators when it is not