by jchw | 2025-05-07 18:49:19+00:00
> Divide by zero must be undefined behavior in any performant language. On x86 you either have a if before running the divide (which of course in some cases the compiler can optimize out, but only if it can determine the value is not zero); or you the CPU will trap into the OS - different OSes handle this in different ways, but most not in a while that makes it possible to figure out where you were and thus do something about it. This just came up in the C++ std-proposals mailing list in the past couple weeks.

I mean look, I already agree that it's not necessarily unreasonable to have undefined behavior, but this statement is purely false. You absolutely can eat your cake and have it too. Here's how:

- Split the operation in two: safe, checked division, and fast, unchecked division.

- OR, Stronger typing; a "not-zero" type that represents a numeric type where you can guarantee the value isn't zero. If you can't eat the cost of runtime checks, you can unsafely cast to this.

I think the former is a good fit for C++.

C++ does not have to do what Rust does, but for sake of argument, let's talk about it. What Rust does here is simple, it just defines divide-by-zero to panic. How? Multiple ways:

- If it knows statically it will panic, that's a compilation error.

- If it knows statically it can not be zero, it generates unchecked division.

- If it does not know statically, it generates a branch. (Though it is free to implement this however it wants; could be done using CPU exceptions/traps if they wanted.)

What if you really do need "unsafe" division? Well, that is possible, with unchecked_div. Most people do not need unchecked_div. If you think you do but you haven't benchmarked yet, you do not. It doesn't get any simpler than that. This is especially the case if you're working on modern CPUs with massive pipelines and branch predictors; a lot of these checks wind up having a very close to zero cost.

> AFAIK all common CPUs have the same behavior on integer overflow (two-complement). However in almost all cases (again, some encryption code is an exception) that behavior is useless to real code and so if it happens your code has a bug either way. Thus we may as well let compilers optimize assuming it cannot happen as it if it does you have a bug no matter what we define it as. (C++ is used on CPUs that are not two-complement as well, but we could call this implementation defined or unspecified, but it doesn't change that you have a bug if you invoke it.)

It would be better to just do checked arithmetic by default; the compiler can often statically eliminate the checks, you can opt out of them if you need performance and know what you're doing, and the cost of checks is unlikely to be noticed on modern processors.

It doesn't matter that this usually isn't a problem. It only has to be a problem once to cause a serious CVE. (Spoiler alert: it has happened more than once.)

> For std::expected - new benchmarks are proving in the real world, and with optimized exception handlers that exceptions are faster in the real world than systems that use things like expected. Microbenchmarks that show exceptions are slower are easy to create, but real world exceptions that unwind more than a couple function calls show different results.

You can always use stack unwinding or exceptions if you want to; that's also present in Rust too, in the form of panic. The nice thing about something like std::expected is that it theoretically can bridge the gap between code that uses exceptions and code that doesn't: you can catch an exception and stuff it into the `e` of an std::expected value, or you can take the `e` value of an std::expected and throw it. In theory this should not have much higher cost than simply throwing.

> As for modules, support is finally here and early adopters are using it. The road was long, but it is finally proving it worked.

Last I was at Google, they seemed to have ruled out C++ modules because as-designed they are basically guaranteed to make compilation times worse.

For CMake, you can't really rely on C++ Modules. Firstly, the Makefile generator which is default on most platforms literally does not and as far as I know will not support C++ Modules. Secondly, it doesn't support header units or importing the STL as modules. For all intents and purposes, it would be difficult to even use this for anything.

For Bazel, there is no C++ Modules support to my knowledge.

While fact-checking myself, I found this handy website:

https://arewemodulesyet.org/tools/

...which shows CMake as supporting modules, green check mark, no notes! So that really makes me wonder what value you can place on the other green checkmarks.

> Long roads are a good thing. C++ has avoided a lot of bad designs by spending a lot of time thinking about problems about things for a long time. Details often matter and move fast languages tend to run into problems when something doesn't work as well as they want. I'm glad C++ standardization is slow - it already is a mess without add more half backed features to the language.

I'm glad you are happy with the C++ standardization process. I'm not. Not only do things take many years, they're also half-baked at the end of the process. You're right that C++ still winds up with a huge mess of half-baked features even with as slow as the development process is, and modules are a great example of that.

The true answer is that the C++ committee is a fucking mess. I won't sit here and try to make that argument; plenty of people have done a damningly good job at it better than I ever could. What I will say is that C faces a lot of similar problems to C++ and somehow still manages to make better progress anyways. The failure of the C++ standard committee could be told in many different ways. A good relatively recent example is the success of the #embed directive[1]. Of course, the reason why it was successful was because it was added to C instead of C++.

Why can't C++ do that? I dunno. Ask Bjarne and friends.

[1]: https://thephd.dev/finally-embed-in-c23