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