Hacker News new | ask | show | jobs
by dataflow 1777 days ago
Don't you need exceptions though? How do you terminate arbitrary operations without exceptions?

Like say you call an algorithm (like std::sort) and during a callback (e.g. in the comparator) you decide to cancel the operation (perhaps user-requested). With exceptions it's easy; you just throw an exception and then catch it. No need to touch or even know the intermediate callers. But without exceptions what do you do? You have to go modify or reimplement the source code of every intermediate function, which is a giant waste of effort at best, and in reality a likely vector for introducing code duplication, brittleness, and bugs.

2 comments

But with exceptions what do you do? You have to go modify or reimplement the source code of every intermediate function to be correct and safe when an exception is thrown at every point where it can be thrown, which is a giant waste of effort at best, and in reality a likely vector for introducing code duplication, brittleness, and bugs.

The point is, retrofitting exceptions onto existing codebase is a lot of pain.

Interruptible functions have the API they have because they have been designed with exceptions in mind for interruptions. If there were no exceptions, the callbacks would have had a different API. A special return value could be used to signal an interruption.

No you don't, you're massively exaggerating. The standard library already has at least basic if not strong exception-safety all over it. And RAII is pretty darn standard practice and guarantees basic exception safety in your own code too. You don't need strong exception safety here, just basic is sufficient for most such cases.

Go try this with std::sort (or std::adjacent_find or whatever) and tell me which of their implementations you had to modify.

Well but of course! These functions are already implemented with basic exception safety in mind. What if they weren't? This is exactly the same situation as a function that has

    callback();
which cannot be changed into

    if Err(error) = callback() {
        return error;
    }
because that would break some invariants.

Changing return type from "void" into some "result" is a mechanical change.

As I already explained: RAII is pretty darn standard practice and guarantees basic exception safety in your own code too. The music is already there and people are already dancing to it.

> What if they weren't?

Obviously the language wasn't designed for rebels. The implicit understanding with tools is that you use them the way they're meant to be used. Only in that case do you get to assume you'll reap the benefits they claim to provide. If you insist on deliberately dancing to a different tune, then you get exactly what you asked for. You can't drive against traffic and then complain people run into you.

There are interesting non-rebel cases of What if they weren't. I have a library (object only, no source) written for C (not C++) which wants callbacks. It is the only interface provided by the vendor for something that shall remain nameless. Every callback has to be wrapped in a try catch, or hell will break loose.
> Changing return type from "void" into some "result" is a mechanical change.

.. but then checking the returned val for error in every call sites is very far from mechanical change. (Attribute about unused return result can help here, with obvious drawbacks.)

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.
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