@divTrunc(d, 4); // d is a i32
instead of
d / 4;
Any chance this gets better?
@divTrunc(d, 4); // d is a i32
instead of
d / 4;
Any chance this gets better?
Thereâs this post that goes into depth on this.
EDIT:
unless iâm misunderstanding you, and you are saying you prefer the former and not the latter.
The closest thing to a âZigâ way I can think of to change this would be something like a @setDivisionBehaviour() builtin that sets the behaviour of the division operator (based on a pre-specified enum) until the builtin is called again or control flow leaves the current block.
That way, instead of using @divTrunc() many times, you might instead do @setDivisionBehaviour(.truncate) and then use the division operator.
To me a required builtin is always a point to stop and think for a second what Iâm doing here.
And I think division is one such case where you should do this.
The truncation behavior of signed division in other languages has caused many long debugging session for me in the past, and itâs not the only problem that integer division can have. There are also performance problems to integer division, especially for runtime-known numbers, and even in your variant a @divTrunc is slower than a @divFloor aka >> 2.
Also in my experience that number of times that you need to divide signed integers is quite small, so in my opinion the advantages outweigh the syntactic noise that come from it. I would be interested to hear what you are doing that makes you need to use signed integer division so much that it bothers you. Iâm sure there is probably a better way to solve your problems without it.
I also prefer unsigned because thatâs what Zig encourages, due to the use of usize for indexing and lengths. @ericlang do you prefer signed ints?
It might help to understand why @as(i32, x) / @as(i32, y) is a compile error in the first place.
When dividing a positive integer by another positive integer, the result should be rounded down. 23 / 5 == 4. Programmers universally agree on and understand this definition of integer division.
But what if the integers are signed and the dividend (the number on the left) is negative? What is -23 / 5? If we choose to round toward 0, which is called truncated division, it is -4. If we choose to round toward negative infinity, which is called floored division, it is -5.
In hardware, signed integer division is most often and most efficiently implemented as truncated division, meaning that -23 / 5 == -4. But on some hardware, floored division is more efficient.
Truncated division is also the definition that C uses since C99 (before that it was implementation-defined!) and most other C-inspired languages have followed suit.
But truncated division is often not the most convenient definition for users, and many people have a mental model that aligns more closely with floored division. This can often result in developers writing buggy code involving / division or % remainder that produce completely incorrect results when passed negative values.
One example: You are developing a 2D game where the world is made out of âchunksâ 100x100 units in size. The player is located at coordinates x = 74, y = 283 and an enemy is stationed at x = -83, y = 210. Do they both occupy the same chunk? Itâs pretty clear that they do not. A naive algorithm for testing this would be to check if player.x / 100 == enemy.x / 100 and player.y / 100 == enemy.y / 100. But this algorithm is wrong and yields true under truncated division, and only works under floored division.
Another (almost canonical) example: You want to check if a number is odd. Easy, just check if x % 2 == 1. But this is once again incorrect because -3 % 2 == -1 under truncated division.
Mistakes like these is why Zig forces you to explicitly state in your code whether you are assuming truncated or floored division when dividing numbers that could be negative, by using @divTrunc() or @divFloor() (or @divExact()). And as another bonus, by having the common / division operator not assume any specific definition, the compiler can choose whichever definition is the most efficient for any given target.
I think my main gripe with Zigâs integer division is that the corresponding builtins for remainder are called @rem and @mod. I believe this is a mistake and that they should be called @remTrunc() and @remFloor(), because the term âmoduloâ is just as ambiguous about rounding direction as âremainderâ and itâs better to have the name of the builtin be explicit.
Note also that I only mentioned negative dividends (left-hand side). It gets even more confusing and unintuitive for humans when negative divisors (right-hand side) are involved. Some programming languages opt for an integer division definition called euclidean division, where the remainder is always >= 0. This is the same as floored division for positive divisors but produces different and sometimes more useful results for negative divisors. Thereâs also ceiling division which is less useful for the common cases but still has its place for certain tasks.
Recommended reading:
I have written the type of code youâre talking about and I can confirm that itâs very easy to get confused when doing integer division with negative numbers.
Unfortunately this would change the meaning of division symbols depending on context, which breaks the radical symplicity that Zig spends so much effort striving for.
To expand on this idea, perhaps you could have @div(a, b, .mode), but is it that any better than just having separate functions? ![]()
I prefer signed ints in some cases because of the nasty builtins, which give me unreadable code.
Sometimes I only need u8 but need to do math on it. And that is âimpossibleâ.
Also very confusing for me:
// does not compile
fn b() void {
var i: i32 = -127;
i += 2;
const d: u8 = 2;
const r = i / d;
std.debug.print("{}\n", .{ r });
}
// does compile
fn c() void {
const i: i32 = -129;
const r = i / 2;
std.debug.print("{}\n", .{ r });
}
That is very confusing. Iâm not sure if that is intended behaviour or not.
It may be confusing, but it has a simple explanation.
In b, you are saying that you are dividing an negative number by a strictly positive number. This requires the semantics as discussed further up in this thread.
In c, you are dividing by a comptime_int which gets promoted to an i32 when you do the division. so in this case, the 2 is used as an i32 which is very different from the d: u8 = 2 case in b.
Because you donât explicitly set it to an unsigned int and it is a comptime known value, the compiler is free to use the typing that it needs to make it work.
Yep. Strange math, strange computers ![]()
I did never realise how a int division by 2 could make such waves.
Iâm with @floooh on this one.
Itâs so rare I actually use the last bit that Iâll rather waste it than loose my sanity over something which is only significant when the full moon is out.
Honestly I would prefer we had u32, i32, and (s32 or n32) with the latter being signed and donât worry about it.
or separate operators, similar to the separate + and +% operators for panic vs overflow addition.
maybe we could have / for truncating division (as is done in most programming languages) and /_ for floor division. then we can also have % for remainder, and %_ for floor-remainder (aka modulus).
Btw somewhat related, this proposal out of the C++ world (from 2018) which argues that âSubscripts and sizes should be signedâ:
I wasnât really aware of this before, but itâs interesting that those things being unsigned quantities in the C++ stdlib was more or less a historical accident.
Wow, what a wonderful piece of advice passed on from the pains of others. While we cannot ensure that the extra bit wonât be useful on tiny devices I think that author is right in identifying that on majority of systems the cons to signed is are insignificant, not even a performance hit. But the cons of unsigned are abundant.
While I really would like to have signed for everything, are the cons true for Zig?
The main problem that C/C++ has is that âunsignedâ is implemented in a way that sucks because of both implicit casting and unchecked overflow.
Zig doesnât suffer from the same problems.
However, I do very much hate usize and all the casting it causes.
Tbf, this problem is arguably worse in day-to-day coding than the implicit signed/unsigned conversion in other languages ![]()
E.g. the implicit conversion in C/C++ rarely causes actual issues (but if it does you can blow your foot off).
OTH the excessive casting in Zig is a constant source of friction (and also reduces readability by drowning the actual expression in âcasting noiseâ). Yes, one can argue that itâs better to avoid this casting by carefully picking the correct integer types, but some language features are hardwired to unsigned integers, so the programmer doesnât have much choice in the matter.
FWIW, in TigerBeetle, where we generally avoid usize, casting hasnât been a problem, outside o the specific issue that for (0...) loop gives you usize indexes (those we often have to cut to u8).
Otherwise, the places where you go from usize to âwhat makes sense for my domainâ are usually a good spot for deploying defense-in-depth assertions. Eg, from yesterdayâs code:
const write_size = bus.io.send_now(
connection.fd.?,
message.buffer[connection.send_progress..message.header.size],
) orelse return;
assert(write_size <= constants.message_size_max);
connection.send_progress += @intCast(write_size);
send syscalls returns me usize, I do a sanity assertion that it canât be larger than the maximal size of the message, and that justifies the cast to u32 on the next line.