CodeRigor

FREE LESSON

Two's complement: signed values, extension & overflow

Encode and decode two's-complement words, sign-extend them, and distinguish unsigned carry from signed overflow without executing C++ signed overflow.

Two's complement assigns the most-significant bit a negative weight. In an eight-bit word its weight is −128; the other weights are 64, 32, 16, 8, 4, 2 and 1. Thus 11111011 means −128+64+32+16+8+2+1 = −5. Its unsigned interpretation is 251. Its hex spelling is FB in either case.

Modern C++ (C++20 and later) specifies two's-complement signed integer representation. This representation rule does not make overflowing signed C++ addition, subtraction or multiplication defined. The lab computes raw word arithmetic in unsigned storage and reports signed overflow separately.

1. Invert and add one, within the width

To form the eight-bit code for −5, start with 00000101, invert the selected bits to 11111010, then add one to get 11111011. Keep only the selected width. Equivalently, for a negative signed value v, its unsigned code is 2^w+v as a mathematical equation. Do not evaluate 1ULL<<64 to implement that equation.

Negate five in eight bits
The +1 distinguishes two’s complement from one’s complement. The low eight bits encode −5.
Negate five in eight bits
The +1 distinguishes two’s complement from one’s complement. The low eight bits encode −5.

The signed range is −2^(w−1) through 2^(w−1)−1. Eight bits cover −128…+127; there is one zero and one extra negative value. The all-one word is −1. The sign-bit-only word is the minimum, −128 in eight bits. Negating that minimum in the same width cannot produce a representable positive value.

Eight-bit code Unsigned One's complement Two's complement
00 0 +0 0
05 5 5 5
7F 127 127 127
80 128 −127 −128
FA 250 −5 −6
FB 251 −4 −5
FF 255 −0 −1

Always state the width and interpretation. A hex numeral beginning with F is not inherently negative; 0xFF is an ordinary positive C++ int literal on this platform. The literal and a modeled eight-bit signed interpretation are different concepts.

2. Extend the sign to preserve the value

Widen a nonnegative word by filling new high positions with zeroes. Widen a negative two's-complement word by repeating its sign bit. Eight-bit FB becomes 16-bit FFFB, preserving −5. Zero-extending it to 00FB gives +251 instead. Byte order is a separate serialization choice after the value and word width are chosen.

Widen the signed word
Sign extension preserves −5. New high bits are all ones for a negative input and all zeroes for a nonnegative input.
Widen the signed word
Sign extension preserves −5. New high bits are all ones for a negative input and all zeroes for a nonnegative input.

The lab's sign_extend accepts equal or larger widths and rejects narrowing. A truncation policy would need a separate contract about lost information.

3. Carry and overflow are independent

The same adder bits can be interpreted as unsigned or signed. Carry reports an unsigned sum leaving the selected word. Signed overflow reports a mathematical signed sum outside its range. Adding equal-sign inputs overflows exactly when the result word has the other sign. Opposite-sign inputs cannot overflow signed addition.

Eight-bit raw addition Low bits Carry Signed overflow
7F + 01 80 no yes: 127+1
FF + 01 00 yes no: −1+1
80 + 80 00 yes yes: −128−128
02 + 03 05 no no: 2+3
Interpret the same result twice
The unsigned result 128 fits in eight bits. The signed mathematical result 128 exceeds +127, so the signed-overflow flag is set.
Interpret the same result twice
The unsigned result 128 fits in eight bits. The signed mathematical result 128 exceeds +127, so the signed-overflow flag is set.

Two's-complement word addition discards an outgoing carry; it does not fold it back as one's complement does. The build returns the low bits even when its overflow flag is set. This is an explicit modeled operation, not a claim that an overflowing native signed expression is safe to evaluate.

4. Write C++ with defined operations

Convert a signed value to uint64_t to obtain its congruent unsigned value, then mask after validating that the signed value fits the selected range. For decoding a negative raw word, mask XOR bits is a nonnegative value at most INT64_MAX. The expression −1 minus that value safely reaches INT64_MIN. This avoids negating INT64_MIN or casting an out-of-range unsigned magnitude to signed.

C++
#include <cstdint>
#include <limits>
#include <print>
int main() {
    const auto all = std::numeric_limits<std::uint64_t>::max();
    const auto raw = static_cast<std::uint64_t>(std::int64_t{-5});
    const auto signed_value = -1 - static_cast<std::int64_t>(all ^ raw);
    std::println("{:016X} {}", raw, signed_value);
}

The example uses value conversions and unsigned arithmetic, not an aliasing pointer cast. Integral conversion rules define the congruent result. For a real C++ type's range, use numeric_limits rather than assuming int has 32 bits.

Do not execute INT_MAX+1, negate INT_MIN, or evaluate an overflowing signed product and then check the result. Validate before native signed arithmetic, use a checked interface, or explicitly use an unsigned word model when that is the intended operation. Fundamental type rules separate signed representation from arithmetic behavior.

5. Build the model and transfer the knowledge

Implement the signed codec, raw adder with independent carry/overflow flags, and sign extension. Check every four-bit addition against wider mathematical values, every eight-bit encoding, and all 64-bit endpoints. The tests never need a signed-overflow expression to create their expected results.

These foundations help with x86 registers, fixed-point firmware, wire formats, masks and checked C++ expressions. Those courses keep their existing prerequisites; this optional sequence adds preparation. Return to the number course or use its flashcards to compare the representations and choose the right interpretation.

Watch & read

Free lectures, talks and readings that cover this skill from another angle. All resources →

Work through the bit arithmetic. Hardware word behavior does not permit overflowing native signed C++ arithmetic; use the lesson's explicit model.