CodeRigor

FREE LESSON

One's complement: negative zero & end-around carry

Model one's-complement signed words explicitly: encode negatives by inverting within the width, preserve both zeros, and add with end-around carry.

A bit pattern needs an interpretation. In an eight-bit unsigned word, 11111010 means 250. In an eight-bit one's-complement signed word it means −5. Neither binary nor hexadecimal spelling alone establishes the signed interpretation.

This lesson models a historical signed-integer representation using unsigned C++ storage. Modern C++ signed integers use two's complement; changing a type name does not switch the machine to one's complement. The model is useful for understanding representations and the end-around addition used in Internet checksums.

1. Encode a negative magnitude

First choose the word width. Write the positive magnitude, then invert every bit inside that width. In eight bits, +5 is 00000101 and −5 is 11111010, or 0xFA. There is no extra +1 step in one's complement.

Negate five in eight bits
The selected width matters: −5 is 1010 in four bits and FA in eight-bit hexadecimal.
Negate five in eight bits
The selected width matters: −5 is 1010 in four bits and FA in eight-bit hexadecimal.

The high bit separates nonnegative from negative encodings. To decode a negative code, invert the width's bits to recover its magnitude, then negate the magnitude. The range is symmetric: for w bits it is −(2^(w−1)−1) through +(2^(w−1)−1). Eight-bit values run from −127 through +127.

Do not apply unary minus to INT64_MIN while trying to obtain its magnitude. It has no positive int64_t counterpart. The one's-complement encode contract rejects it even at width 64 because it lies below that model's minimum. Validate the range before taking a negative magnitude.

2. Two encodings of zero

Inverting the all-zero word produces the all-one word. Both represent numeric zero: 00000000 is positive zero; 11111111 is negative zero. In a four-bit model, −0 is 1111, not 1000. That latter spelling belongs to sign-magnitude's negative zero and must not be confused with this representation.

Keep value and encoding information
A C++ integer value cannot distinguish these two zeros. The lab decoder returns a separate negative_zero flag.
Keep value and encoding information
A C++ integer value cannot distinguish these two zeros. The lab decoder returns a separate negative_zero flag.

The build's Decoded record contains an int64_t value and a bool negative_zero. Encoding the numeric value 0 produces positive zero. Decoding the all-one word preserves its negative-zero identity in the flag. A raw-word addition keeps whichever zero encoding the arithmetic produces, rather than silently normalizing it.

Bitwise complement (~) is not logical negation (!). Logical negation asks whether a value is zero and returns bool. Bitwise NOT inverts all value bits of its promoted operand. To flip only eight modeled positions in a wider unsigned value, XOR with the mask 0xFF or AND the complement with that mask.

C++
#include <print>
int main() {
    constexpr unsigned word = 5;
    constexpr unsigned mask = 0xFF;
    std::println("{:02X} {}", word ^ mask, !word);
}

3. End-around carry changes addition

Add the raw words. If there is an outgoing carry, add it back into the least significant position. Consider four-bit 5 + (−3). The codes are 0101 and 1100. Their unsigned sum is 10001. Keep 0001, then add the outgoing 1, giving 0010, which represents +2. Simply discarding the carry would give the wrong value.

Fold the outgoing carry
End-around carry adds the outgoing bit at the right. Two’s-complement word addition will instead discard that carry.
Fold the outgoing carry
End-around carry adds the outgoing bit at the right. Two’s-complement word addition will instead discard that carry.

The zero result is instructive: four-bit 0101 + 1010, the codes for 5 and −5, produces 1111, the negative-zero code, without a carry. Meanwhile 0000 + 0000 produces positive zero. Distinguish numeric equality from identical bit patterns.

C++
#include <print>
int main() {
    constexpr unsigned a = 5, b = 12;
    constexpr unsigned total = a + b;
    constexpr unsigned folded = (total & 15u) + (total >> 4);
    std::println("{} {}", total, folded);
}

This algorithm describes a word arithmetic model. Like other fixed-width arithmetic, it does not make every mathematical signed sum representable. For instance, four-bit +7 plus +1 is outside −7…+7. The build returns a raw word and does not claim to be a checked signed-add API.

4. Model the full-width case safely

For widths below 64, two width-sized operands can be added in uint64_t and the outgoing carry read above the selected width. At width 64, unsigned addition is modulo 2^64. Detect the carry by comparing the wrapped sum with one operand, then add that carry to the low word. Never shift a uint64_t by 64 or try to store the mathematical value 2^64 in it.

The lab validates widths 2–64 and rejects raw bits outside the selected width. Exercise both zeros, a mixed-sign sum, maximum magnitudes, and a carry at width 64. Read the specification for exact error behavior.

RFC 1071 describes the Internet checksum's one's-complement sum and end-around carry. A complete checksum also has byte pairing, odd-length handling and final-complement rules; the lab here implements the underlying word operation, not that whole protocol. Next: two's complement.

Watch & read

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

Compare signed representations and the zero problem. The lesson and lab make one's-complement width and carry behavior explicit.