Two's Complement: One Circuit for Addition and Subtraction
Negative numbers are not stored with a minus sign. They are stored so that ordinary binary addition produces the right answer without the hardware ever knowing a value was negative — which is why subtraction needs no separate circuit.
Software view, hardware view
The gap between what you wrote and what the machine does is where this whole domain lives.
Why not just use a sign bit?
The obvious design — reserve the top bit for the sign and store the magnitude below it — is called sign-magnitude, and it fails for practical reasons rather than aesthetic ones. It produces two encodings of zero, positive and negative, so every equality comparison needs a special case. Worse, addition needs to inspect both signs and decide whether to add or subtract magnitudes, which means real logic rather than a plain adder.
Two's complement solves both. To negate a value, invert every bit and add one. The representation has a single zero, the top bit still indicates negativity as a side effect, and — the decisive property — ordinary unsigned binary addition produces the correct signed result with no modification. The same adder circuit serves both interpretations.
The table below shows the three candidate schemes on four bits. Watch the zero column and the addition column: two's complement is the only one where the hardware needs no special handling for either.
| Scheme | How −5 is stored | Encodings of zero | What addition costs in hardware |
|---|---|---|---|
| Sign-magnitude | 1101 (sign bit + magnitude 5) | Two: 0000 and 1000 | Inspect both signs, then add or subtract magnitudes — real logic |
| One's complement | 1010 (invert 5) | Two: 0000 and 1111 | Plain addition, plus an end-around carry correction step |
| Two's complement | 1011 (invert 5, add 1) | One: 0000 | Plain unsigned addition, unmodified — no correction at all |
Subtraction disappears into addition
The payoff is that a CPU does not need a subtractor. To compute a - b, the hardware negates b — invert the bits, add one — and feeds it to the adder. The "add one" is free because the adder already has a carry-in input, so negation costs one row of NOT gates and setting a bit that already existed.
This is why Building an Adder: Where Arithmetic Comes From is genuinely the foundational arithmetic lesson: once you have an adder, you have subtraction, comparison (subtract and inspect the flags) and the basis of multiplication. It is also why the same instruction encoding often serves both signed and unsigned addition — the bit pattern produced is identical, and only the *interpretation* of overflow differs.
The worked example below shows the mechanical process. Note that the final carry out of the top bit is simply discarded, and that discarding it is exactly what makes the arithmetic come out right.
Step 1: represent 17
17 = 0001 0001
Step 2: negate it (invert all bits, then add one)
invert = 1110 1110
add 1 = 1110 1111 <- this is -17
Step 3: add 42 and -17 with a plain unsigned adder
0010 1010 (42)
+ 1110 1111 (-17)
------------
1 0001 1001
^
carry out is discarded
Result: 0001 1001 = 25 correct
The adder did no sign checking. It added two 8-bit patterns and
threw away the ninth bit; the encoding makes that produce the
right signed answer.The asymmetry, and the edge case it creates
Two's complement has one visible quirk: the range is not symmetric. In eight bits the values run from −128 to +127, so there is one more negative number than positive. That falls directly out of having a single zero — the encodings have to go somewhere, and with 0000 0000 used once rather than twice, one extra pattern is available on the negative side.
The consequence is a real edge case rather than a curiosity. The most negative value has no positive counterpart, so negating it cannot produce a representable result. Applying the invert-and-add-one procedure to it returns the same value unchanged, which means an absolute-value function can return a negative number — a genuine source of bugs in range-checking and parsing code.
The related trap is mixing signed and unsigned in a comparison. Most languages convert the signed operand to unsigned, at which point a small negative number becomes an enormous positive one and the comparison silently produces the opposite of what was intended. This is a frequent cause of bounds checks that pass when they should fail.
1// 1. The most negative value has no positive counterpart.2// In 32-bit two's complement the range is -2147483648 .. 2147483647.3const INT32_MIN = -21474836484// abs(INT32_MIN) is not representable. Invert-and-add-one on5// 1000...0000 returns 1000...0000 -- the same value, still negative.6// In C or C++ this is undefined behaviour; in Java it silently7// returns INT32_MIN; in Python integers grow so the case vanishes.8 9// 2. Mixed signed/unsigned comparison converts the signed side.10// A negative number becomes a very large positive one.11function looksSafe(index: number, length: number): boolean {12 // If this were C with an unsigned length, index = -1 would be13 // converted to 4294967295 and this check would PASS.14 return index < length15}16// The fix is to check both ends explicitly, and to prefer signed17// types for anything that can meaningfully be negative:18function actuallySafe(index: number, length: number): boolean {19 return index >= 0 && index < length20}Key points
- Two's complement negates by inverting every bit and adding one, giving exactly one encoding of zero.
- Its decisive property is that ordinary unsigned binary addition produces the correct signed result unmodified.
- Subtraction is implemented as addition of a negated operand, so no separate subtractor circuit is needed.
- The range is asymmetric — one more negative value than positive — because zero occupies only one encoding.
- The most negative value cannot be negated, and mixed signed/unsigned comparison silently converts the signed side.
Follow the mechanism
The path through the machine, hop by hop — and the conclusions it invites that are wrong.
- 1Value → encoding: a negative number is stored as the bitwise inverse of its magnitude plus one.
- 2Subtract instruction → negation: the hardware inverts the second operand with a row of NOT gates.
- 3Carry-in → adder: the "+1" is supplied through the adder's existing carry-in, costing no extra hardware.
- 4Adder → result: the sum is computed as if both operands were unsigned; the carry out of the top bit is discarded.
- 5Flags → subsequent branch: the same operation sets condition flags that a later conditional branch reads — see Control Hazards: The CPU Does Not Know Where You Are Going.
- • Believing the top bit is a sign flag the hardware consults; it is a consequence of the encoding, not a separate field.
- • Assuming the signed range is symmetric and writing bounds checks that omit the extra negative value.
- • Expecting absolute value to always return a non-negative result.
- • Assuming a comparison between a signed and an unsigned value compares them numerically as written.
Consequences, controls and cost
- • One adder circuit serves addition, subtraction and comparison, which saves substantial silicon.
- • Signed and unsigned addition produce identical bit patterns; only overflow detection differs between them.
- • Absolute value has an unrepresentable case that behaves differently in every language.
- • Mixed signed/unsigned comparisons produce silently wrong results, a recurring source of failed bounds checks.
- • Prefer signed types for quantities that can be negative, and avoid mixing signedness in a single comparison.
- • Guard both ends of a range explicitly rather than relying on a single upper-bound check.
- • Enable sign-comparison and sign-conversion warnings; compilers detect most of these cases.
- • Treat the most negative value as an explicit case in any code that negates or takes absolute values.
- • Compile with sign-conversion warnings enabled and treat them as errors in code that does bounds checking.
- • Unit-test the extreme values of every integer type you use — most sign bugs appear only at the boundaries.
- • Inspect the disassembly for a subtraction and confirm it became an add with an inverted operand.
- • The asymmetric range is a permanent wart that every language must decide how to handle, and they disagree.
- • Using wider types to avoid edge cases costs memory and cache footprint.
- • Signed arithmetic in C and C++ makes overflow undefined, which enables optimisation but removes the wrapping guarantee unsigned types have.
Scope
§224 — what these claims are specific to.
- GENERALTwo's complement is used by essentially all contemporary hardware, and C++20 mandates it for signed integer representation. Historical machines used sign-magnitude and one's complement.
- PLATFORM-SPECIFICWhat happens at the edges is language-defined, not hardware-defined: signed overflow is undefined behaviour in C and C++, wraps in Java and C#, and cannot occur in Python because integers are arbitrary precision.
Misconceptions
Where the rest of this lives
The hardware wraps predictably, but what the *language* permits differs sharply — signed overflow is undefined in C and C++ specifically so the compiler may assume it never happens. That domain does not exist yet.