Is that true though? Placing exception handlers in the stack, so examining every stack frame for one, is equivalent to testing the sum type to see if it is in error state, surely?
My disclaimer: I know very little about compilers, so this is an actual question.
Anyway, the typical implementation involves two phases, one which uses a table to identify the matxhing catch clause, then another one going through landing pads for each frame of the stack. Just consult the Itanium ABI spec for technical details.
Here's a simple example showing exceptions vs. sum types: https://godbolt.org/z/9Er6eKxs9
In the non-throwing exception path, there's literally no error or exception handling code executed at all. Whereas in the sum-type error-returning version, you have a branch at every call site that's always executed regardless of if there's an error or not.
Now the exception handler generates ".cold" clones of the function, so the total assembly for the exception handling one is larger. However, that assembly isn't every executed if an exception isn't thrown, which is the broader point. So it's not taking up CPU cache space & it's not taking up branch predictor slots.
In C++, they are not. Since C++ allows objects to be created on the stack within the each current scope, every constructor call for such a new object has to register its destructor with the exception handler's object cleanup jump tables.
Consider an edge case with a loop where 100k local objects are created. Each constructor invocation will incur an overhead of an indexed memory store of the destructor's address in the exception handler's table[0] for each object.
An indexed memory store is typically 1x instruction for a CISC architecture (it can be more if the data section is located too «far» in the address space, and the ISA limits the offset width in the instruction encoding). It is typically several load low and shift, load low and store instructions on a RISC architecture. L1 D-cache and L2 cache, sometimes L3 cache as well (if the exception table grows large), and TLB reloads[1] get involved at all times. All of the aforementioned is just to register a destructor for an exception that might not occur. So, no, it is not free and (occasionally) the time dimension is not even clearly defined.
This is rather unique and specific to C++ because it is an outlier and allows new objects to be created on the heap AND also on the local stack. Languages that allow new objects to be only created on the heap do not incur such an overhead.
[0] Bonus point: 100k local objects being created inside a try/catch block will also blow the jump table out of proportions and add more cache pressure and cache line reloads.
[1] And even page faults might occur – if the exception table crosses memory pages.
(In theory a compiler-writer could try to reuse destructor-calling code between the normal exit case and the exception case, but that might force one extra branch in the non-exception case.)
This is incorrect. With error return values you're adding a branch to every function call which is quite expensive on the whole. You're adding i-cache pressure & you're adding branch prediction pressure.
Exceptions in nearly every language that supports them (including C++) don't go through return values at all. Rather when thrown the stack is walked to find an exception handler. So exceptions are more expensive to throw than return values, but completely free when not thrown unlike return values.
> It very much isn't, part of the system programming workload is to provide reusable components.
There's absolutely no issue with exceptions & library boundaries in general. Statically checked exceptions also exist (see Java - although there's a big debate on if that's a good idea or not, but also see https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p07... ), I'm not sure why you're arguing as if they don't.