Hacker News new | past | comments | ask | show | jobs | submit login

Given their example about classes and "don't show the key before showing the lock", it's ironic the writer pans languages aiming to be "simpler" without understanding the kind of complexity they're actually avoiding. Zig, Go, (and Java and C# and other not as sexy and modern examples) are avoiding C++'s complexity - features with complicated interactions between each other that can sink your program without you being aware at all.

My favourite example is how the integer promotion rules make multiplication of unsigned shorts undefined behaviour, even though unsigned integral numbers have well-defined wrap-around arithmetic. Going by [1], we have the following rules:

1. Usual arithmetic conversions: The arguments of the following arithmetic operators undergo implicit conversions for the purpose of obtaining the common real type, which is the type in which the calculation is performed: (...) binary arithmetic: , /, %, +, -; (...) Both operands undergo integer promotions .

2. If int can represent the entire range of values of the original type (or the range of values of the original bit field), the value is converted to type int

3. (added after the above rules existed for the purposes of better optimizations) If the result of an arithmetic operation on two ints over- or underflows, that is Undefined Behaviour.

4. 0xFFFF 0xFFFF = 0xFFFE001 > INT_MAX

5. Therefore, two unsigned short values cannot be safely multiplied and must be explicitly cast to "unsigned int" each time.

[1] https://en.cppreference.com/w/c/language/conversion




The problem with implicit wrap-around of unsigned integer is that it makes your program slower. The underlying hardware with 2s complement does not need to pay the cost of reserving the overflow space or shifting/masking it out.

Further more, its more consistent if signed and unsigned behaving identical than having to look for the type with integer promotion rules of the number.

If you prefer the current C/C++ way of less performance and inconsistency that is fine.


And now I see that HackerNews ate my multiplication symbols. What I meant to type is that

    unsigned short a = 0xFFFF;
    unsigned short b = a;
    unsigned short c = a * b;
Is currently undefined behaviour on platforms where int has more bits than short (like x64 and arm) due to the interaction between the integer promotion rules and undefined integer overflow.


Rust here says OK, we'll define Mul (the * operator) for the same type (and for references to that type) so

  let a: u16 = 0xFFFF;

  let b: u16 = a;

  let c: u16 = a * b;
... is going to overflow, Rust would actually detect that because 0xFFFF is a constant, so, this says "Silently do overflowing arithmetic" and er, no, it doesn't compile. However if you achieved the same thing via a blackbox or I/O Rust doesn't know at compile time this will overflow, in a Debug build it'll panic, in a Release build it does the same thing as:

  let c: u16 = a.wrapping_mul(b);
Because the latter is not an overflow (it's defined to do this), you can write that even in the constant case, it's just 1, the multiplication evaporates and c = 1.

In C++ if you can insist on the evaluation at compile time there is no UB, so you get an compiler error like Rust.


I don't like Rust's approach, but it is better than C's. Rust should either commit to wraparound or make the default int type support arbitrary values.

In C, the problem isn't the silent wraparound, the problem is that when the compiler sees that expression, it will assume that the resulting value is less than INT_MAX, and optimise accordingly. The other insidious problem is that wraparound is defined for other unsigned arithmetic, so a programmer that hasn't had this explained to them, or read the standard very carefully, would quite easily assume that arithmetic on unsigned short values is just as safe as it is for unsigned char, int or long, which is not the case.


I understand why you don't like C's behaviour here.

> Rust should either commit to wraparound or make the default int type support arbitrary values.

Committing to wrapping arithmetic everywhere just loses the ability to flag mistakes. Rust has today Wrapped<u32> and so on for people who know they want wrapped arithmetic. I'd guess there's a bunch of Wrapped<u8> out there, some Wrapped<i8> and maybe some Wrapped<i16> but I doubt any large types see much practical use, because programmers rarely actually want wrapping arithmetic.

The mistakes are real, they are why (thanks to whoever told me about this) C++ UBSAN in LLVM actually flags unsigned overflow even though that's not actually Undefined Behaviour. Because you almost certainly weren't expecting your "file offset" variable to wrap back to zero after adding to it.

For performance reasons your other preference isn't likely in Rust either. Type inference is not going to let you say "I don't care" and have BigNums in the same code where wrapping is most dangerous.

We can and should teach programmers to write checked arithmetic where that's what they meant, and Rust supports that approach much better than say C++. Also the most serious place people get tripped up is Wrangling Untrusted File Formats and you should use WUFFS to do that Safely.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: