Making two's complement part of C's integer contract
N2330 proposes a single signed integer representation while preserving important differences between representation, conversion and arithmetic.
C's integer model permits representations that mainstream compiler users rarely encounter. Removing those alternatives requires decisions about the operations programmers perform on integers, as well as the bits that store them. JF Bastien and Jens Gustedt's N2330 proposal, dated January 15, addresses that problem as work on C2x continues. The WG14 document log lists it under the proposed move to two's complement sign representation. It remains a proposal, so existing C programs cannot treat its wording as an adopted language guarantee.
C was my first language, and its small specification still appeals to me. The difficulty is that understanding an expression requires knowing which properties the language guarantees and which properties belong to a particular implementation. This proposal interests me because it reduces that distinction for a fundamental type without pretending that every arithmetic operation becomes safe.
Why the old alternatives matter
Bastien's earlier N2218 paper, published in March 2018, describes the mismatch between the standard and contemporary implementations. C permits sign and magnitude, ones' complement and two's complement. The paper reports that MSVC, GCC and LLVM do not support the other representations. Its argument for change concerns the cost of retaining theoretical portability when real programs already assume the common representation.
Those alternatives affect reasoning about negative values. Sign and magnitude assigns a separate sign to a magnitude. Ones' complement negates a value by complementing its representation. Both can have a negative zero. Two's complement instead gives the sign position a negative weight. In a hypothetical eight-bit value representation, the pattern with only that position set represents minus 128, while the largest positive value is 127. This asymmetry explains why the negative endpoint requires care when negating a signed value.
N2218 also distinguishes ordinary signed arithmetic from the representation proposal. Keeping overflow undefined permits an implementation to reason about expressions using representable mathematical results. A representation describes how valid values occupy storage. It does not by itself specify what a program does when an expression's mathematical result lies outside the type's range. Confusing these two questions produces incorrect expectations even on a machine whose instructions wrap.
What the C++ wording contributes
The related C++ work makes the intended representation relationship explicit. Jens Maurer's November 2018 P1236R1 wording describes corresponding signed and unsigned types using the same range exponent. A signed value and the unsigned value congruent to it modulo the corresponding power of two have matching value bits. Its example relates signed minus one to the largest unsigned value.
For a hypothetical eight-bit type, unsigned 255 and signed minus one therefore use the same value bits. That statement does not require a program to reinterpret storage through an incompatible pointer. It describes a relationship between values and representations. A numeric conversion and an arbitrary pointer cast are different operations, and an integer representation proposal supplies no general permission to disregard other language rules.
The C++ wording also leaves the choice of plain char signedness to the implementation. Requiring two's complement consequently does not make every character object signed. That is a useful limit on the scope of this work. A program that interprets raw bytes still has to choose its types deliberately, even when signed integer representation becomes more predictable.
Representation does not define every operation
The existing rules are visible in the C11 committee draft N1570. Section 6.3.1.3 distinguishes conversions by the destination type. A representable value stays unchanged. Conversion to an unsigned type reduces a value into that type's range. An unrepresentable conversion to a signed type instead permits an implementation-defined result or an implementation-defined signal.
That distinction already gives a portable way to express an unsigned endpoint without depending on the source machine's signed representation. The conversion in this function supplies the endpoint used by the comparison:
#include <limits.h>
unsigned int endpoint(void)
{
return (unsigned int)-1;
}
int endpoint_matches(void)
{
return endpoint() == UINT_MAX;
}
The result follows from the conversion rule. It does not establish that incrementing an int past INT_MAX is valid. Section 6.5 of the same draft makes an exceptional expression result undefined when it lies outside the representable range. Section 6.5.7 gives separate rules for shifts, including restrictions on the shift count and signed left operand. Treating these provisions separately is necessary when explaining any proposed simplification of integer storage.
An unsigned counter expresses wraparound without waiting for the proposal. The following function adds one at the upper endpoint of its type:
#include <limits.h>
unsigned int wrap_endpoint(void)
{
unsigned int value = UINT_MAX;
return value + 1u;
}
This example returns zero under the existing unsigned arithmetic rules. Changing the local variable and constant to their signed counterparts would change the semantic question. The fact that a processor can execute an addition instruction says little about which transformations a C implementation may apply to an expression with undefined behavior.
What N2330 actually narrows
N2330 seeks interchangeability with the C++ representation changes, including consistent widths for corresponding signed and unsigned types. It also removes the possibility of treating the most negative two's complement pattern as a trap. These changes reduce the number of cases involved in describing integer data at language boundaries.
The proposal handles operations selectively. It retains the possibility of a signal for an out-of-range signed conversion, otherwise specifying the corresponding modulo result. It proposes arithmetic right shift for signed negative values. Signed left-shift overflow remains undefined because the authors identify optimizer dependence on the existing rule. These choices show why describing the paper merely as mandatory two's complement misses consequential parts of its design.
I value this direction most for interoperation. C is a reference point for foreign function interfaces, so explicit relationships between signed and unsigned representations help explain shared data. They also make disagreements easier to locate. Representation compatibility does not settle calling conventions or every structure layout decision, and a program still needs the platform's interface contract.
For existing code, the immediate distinction is between guarantees already supplied by a chosen implementation and guarantees proposed for the language. A compiler documenting two's complement can already support code that depends on that representation. N2330 would reduce the implementation-specific assumptions needed to explain such code. Its treatment of signed left shift preserves an operational boundary that remains relevant even after the representation alternatives disappear.