#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <time.h>
int main() {
srandom(time(NULL));
unsigned int a = random() & 0xffffffff;
unsigned int b = 0;
unsigned int c = a + b - 1;
a += b - 1;
assert(a == c); // true.
return 0;
}
In Zig:
pub fn main() void {
var prng = std.Random.DefaultPrng.init(@intCast(std.time.microTimestamp()));
var rand = prng.random();
var a: u32 = rand.int(u32);
const b: u32 = 0;
const c = a + b - 1;
a += b - 1; // error: overflow of integer type 'u32' with value '-1'
std.debug.assert(a == c);
}
I donât know why Zig is designed to be so different from C.
You are trying to do â0 - 1â on an unsigned integer, itâs obviously will return an error. Although I donât know why it says overflow, maybe it should be underflow.
Itâs a long and detailed story, but the TL;DR is that Câs integer math expressions are too sloppy and have a surprising amount of footguns.
Some differences:
C has integer promotion to 32-bit int (even on 64-bit systems). This is responsible for most of the convenience in Câs integer math, but also has footguns (especially when an implicit sign conversion is involved). Zig has no integer promotion at all.
C happily casts between integer types of different lengths and signs, Zig restricts this to implicit casts which cannot lose information (this at least is a bit more convenient than Rust which forbids implicit casts entirely)
C silently wraps around unsigned integers, in Zig unsigned overflow is runtime-checked (in debug and release-safe), what Zig offers as alternative is explicit wrapping operators (e.g. +%) - and also saturating operators (e.g. +|)
�
âŚapart from those obvious differences, Zig is definitely more strict than it should be here and there and also has some surprising behaviours even if one is aware of the differences to C, but there are plenty of proposals in flight which try to fix those problems without giving up the idea that integer math shouldnât inherit Câs footguns.
it is safer, especially for those unfamiliar with these hidden rules
it gives more optimization opportunities to the compiler, here is a simple example of one such optimization that C is not allowed to do: Compiler Explorer
In C, addition and subtraction of unsigned numbers are wraparound addition and wraparound subtraction.
In zig, the + and - operators for integers, whether signed or unsigned, are ordinary addition and ordinary subtraction. Therefore, adding beyond the maximum representable range or subtracting below zero is illegal. However, you can explicitly use the wraparound operators +% and -% to represent wraparound addition and wraparound subtraction. Therefore, in your zig program, changing +=, + and - to +%=, +% and -% will achieve the same behavior as in C.
As for the reason for this design, C couples the use of integers, and whether they should wrap around, with their range. It assumes that if the user requires a non-negative integer range, then they should wrap around. This often doesnât align with real-world needs; a non-negative integer range doesnât necessarily mean they want an integer with wrap-around properties.
Decoupling whether an operation wraps around from the integerâs range is the design goal of zig: if you want an integer to wrap around, use explicit wrap-around operators.
pub fn main() void {
var prng = std.Random.DefaultPrng.init(@intCast(std.time.microTimestamp()));
var rand = prng.random();
var a: u32 = rand.int(u32);
const b: u32 = 0;
const c = a +% b -% 1;
a +%= b -% 1; // no error, we have specified wrapping arithmetic with +%
std.debug.assert(a == c);
}
Does that help?
(Edited to properly handle the case where a == 0, thanks @Olvilock!)
Sorry. I thought the code example is detail enough.
In C, a += b - 1 is same as a = (a + b) - 1, but in zig, a += b - 1 is same as a = a + (b - 1). So it is why Zig reports error in my example code. If Zig were like Câs way, it will not report error. And it is what I am wondering why Zig deign is so anti-intuitive.
Itâs not actually associativity of operations, itâs that Zig doesnât do wrapping overflow when + is used. The contract of + is that the result must fit into the result location, otherwise it is safety-checked illegal behavior.
In C, + for unsigned values wraps around, thatâs why your formula works. In Zig, we spell that +%.
Your understanding of the meaning of a += b - 1 in C is incorrect. Even in C, a += b - 1 clearly means a = a + (b - 1).
If youâre unsure, consider the result of a -= b - 1 in C. It should mean a = a - (b - 1), but according to your understanding, it becomes a = (a - b) - 1, which clearly contradicts the actual result.
The fundamental reason this works in C is that C wraps around unsigned integer operations. If you use signed integers instead of unsigned integers, operations on the edge of overflow become illegal. Of course, even if you perform undefined behavior in C, you generally wonât receive an error.