Hacker News new | ask | show | jobs
by ribit 1777 days ago
I agree that a modern high-level programming language model needs an ergonomic error model. But C++ exceptions are not the only way to go. You can have error model that have similar (or even better ergonomy) than C++ while not having any of the drawbacks (like extremely complicated runtime stack, slow exception handling, messed up control flow etc.). Basically, in my personal opinion, any error handling that involves automatic stack unwinding is a failure.
2 comments

Note you didn't really answer my question at all.

Right now lots of algorithms like std::search, std::find_if, etc. are not only exception-safe, but in fact exception-agnostic. Neither the algorithm, nor you, need to know a priori if your predicates will throw exceptions (which are things that may be literally impossible to know upfront), and yet despite that, (a) the algorithms will work completely correctly if any exception is thrown, (b) if you do need to do something like canceling the operations in the middle, you have a means to do that via exceptions, and (c) you will get extremely high performance as long as you don't throw an exception. That's a lot of flexibility even the most trivial implementations of many such algorithms get absolutely for free. (!) I don't know about you, but to me the fact that I can suddenly decide to "cancel" many functions halfway despite their authors never having to even think about that possibility is pure awesomeness.

So I asked "how would you do achieve {the benefits of the exception model} without exceptions" but you just said "it is possible" and... left me hanging. Well if that's really true, then how?

> You can have error model that have similar (or even better ergonomy) than C++ while not having any of the drawbacks

I don't buy it. Unless you're intentionally allowing yourself to introduce drawbacks that never existed in C++'s model. If you're really saying you can find a strictly better solution, then we're all definitely interested in hearing... and I'll believe it when I see it.

You have to realize ergonomicity (word?) isn't the only axis here. Performance is also a big one, and C++ is designed for maximizing performance in non-exceptional executions. I don't know what error models you're thinking of, but anything along the obvious stuff I've seen (like the usual "replace T with maybe<T>/optional<T>/fancy<T>") would come with far greater performance hits even in the 'happy' paths than C++ has (not to mention potential increases in memory usage, etc. in more complex cases), and even their ergonomics would be debatable depending on the situation.

> Neither the algorithm, nor you, need to know a priori if your predicates will throw exceptions (which are things that may be literally impossible to know upfront)

I don't think this property is desirable at all. I prefer to know whether a function can or cannot result in an error, ideally encoded within the type system. The C++ "everything can throw" paradigm yo describe here obfuscates the program logic and promotes bad coding practices. I know, C++ programers like to argue that "everything throws" is a natural property of any real world code, but somehow folks are able to work with Rust and Swift without too much hassle.

> If you're really saying you can find a strictly better solution, then we're all definitely interested in hearing... and I'll believe it when I see it.

A strictly better solution has been found long time: error sum types.

> Performance is also a big one, and C++ is designed for maximizing performance in non-exceptional executions. I don't know what error models you're thinking of, but anything along the obvious stuff I've seen (like the usual "replace T with maybe<T>/optional<T>/fancy<T>") would come with far greater performance hits n the 'happy' paths than C++ has (not to mention potential increases in memory usage, etc. in more complex cases)

This is again a very popular argument I've seen used by many in the C++ community, but the simple fact is that this argument is simply not true. Already very naive result type implementations using C++ show no measurable performance difference in the "good" path (with a non-trivial function), and using an optimized calling convention makes error sum types zero-cost on modern hardware.

For example, Swift uses a dedicated register to signal exceptional function result. On the "good" path, you have to zero this register in the callee and conditionally jump on its value in the caller. These operations are essentially free on any modern CPU with superscalar execution, register renaming and branch prediction. The only cost is a register and a few extra instructions which won't carry any performance impact. One can optimize this even further by using condition flags to signal exceptional result (frees up a register and saves an instruction).

To sum it up, using result types with optimized calling conventions gives you the same performance as the C++ exceptions on the good path, much better performance on the exception path, saves space (few bytes of extra instructions take much less space than the unwind information), radically simplifies the compiler (no long jumps, functions enter and exit regularly), radically simplifies cleanup (function exits regularly and can run destructors as usual), simplifies the control flow and so on.

In fact, the only disadvantage I see with this implementation is that exception propagation might be slower than a longjump if you have hundreds of nested functions. But I think you have much bigger problems if you call stack looks like that...

IMO algebraic data types pretty much solve everything that exceptions try to solve.

while also encoding into the type system that it can fail/what failure modes there are, while also forcing you to handle it locally.

Local handling is, of course, an anti-pattern in code using exceptions, and not to be encouraged. It's rare that you handle exceptions; normally, you just abort what you're doing and unwind.

If you have an API which fails often enough that you want to handle exceptions from it, it probably shouldn't use exceptions, and use some kind of conditional result or ADT equivalent instead. A concrete example would be the TryParse methods in .NET.

I think that it is much better to return an adt with "common exceptional cases" and reserve exceptions for the truly exceptional ones. For example, a lost network connection shouldn't be one, but a put of memory one makes sense.

Local handling was meant in terms of locally seeing pitential errors