C++ Exceptions: Under the Hood (2013)(monkeywritescode.blogspot.com) |
C++ Exceptions: Under the Hood (2013)(monkeywritescode.blogspot.com) |
1. they are very hard to understand all the way down
2. they are largely undocumented in how they're implemented
3. they are slow when thrown
4. they are slow when not thrown
5. it is hard to write exception-safe code
6. very few understand how to write exception-safe code
7. there is no such thing as zero-cost exception handling
8. optimizers just give up trying to do flow analysis in try-exception blocks
9. consider double-fault exceptions - there's something that shows how rotten it is
10. has anyone yet found a legitimate use for throwing an `int`?
I have quit using exceptions in my own code, making everything 'nothrow'. I regret propagating exception handling into D. Constructors that may throw are an abomination. Destructors that throw are even worse.
[1] I attended a presentation on it by MS soon after Win64 came out. All I could think of at the end was "what's a cubit". I understood exactly nothing about it.
Walter, what do you think about Herb Sutter "new" C++ exceptions (which is basically about throwing an int/a word) ?
() they solve a handful of use-cases really really well.
() if you are writing relatively decent C++, most code is pretty much exception safe already
() lots of abstractions are dangerous when mis-used
() they are sufficiently low cost that they almost never show up in the fairly extensive perf profiling I do on a large real-world application (LibreOffice). And LibreOffice throws exceptions __a lot__
(*) Except for toolchain writers, nobody cares how they are implemented
They're terrible for this. C++ Exceptions are a perfectly good exception mechanism, but what C++ programmers are trained to do with them isn't exceptions but error handling and they're not suitable for that.
"I tried to create the file but it already existed" is an error - and now you're going to write the unhappy path code, this is the wrong place to have exceptions.
"I tried to create the file but the OS now says the abstract concept of files is alien to it" is an exception. You are not prepared for this eventuality, the best you can do is explain to the user as best possible what happened and hope a human knows what to do.
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.
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.
Reasonnable compromise, or should we get rid of all unwinding always? And if so, do we abort() or do we ask users to handle any and all possible errors.
The usual way control flow progresses is forwards, but when an error occurs, it goes backwards, over the defers and errdefers.
C++ exception handling and other languages with destructors force you to do this declaratively, but then don't give enough control over exactly the situations they matter in: setup and teardown.
Meanwhile with explicit control, you just encode exactly what happens in the "backwards" control flow. No surprises, no trying to figure out what happens based on declarative rules. Once I figured this out, I was able to use it to simplify the logic of some things in the self-hosted compiler that are extremely error prone in the C++ implementation:
* Lazy source locations: passing in "none" for a source location, and then handling the "error.SourceLocationNeeded" and then doing the expensive calculations to find the source locations before retrying the operation.
* Generic instantiations: returning "error.GenericPoison" for when a type parameter cannot be determined without information from the callsite. In this case, the analysis is cleanly aborted and function marked as generic.
I'm pleased with how this turned out, and I've started to think of other languages in terms of how they map to this "forwards" and "backwards" control flow concept.
What I personally use is the "poisoning" technique. This involves marking an object as being in an error state, much like a floating point value can be in a NaN state. Any operation on a poisoned object produces another poisoned object, until eventually this is dealt with at some point in the program.
I've had satisfactory success with this technique. It does have a lot of parallels with the sum type method.
In short, while the problem is mitigated somewhat compared to C++, it's still one of the most common causes of bugs in unsafe Rust code.
Rust programs can choose to abort on all panics, rather than unwind. Firefox does this, for example.
That's like saying "garbage collecton is slow and complex, just use malloc() and free() instead".
There are other very good solutions that involve explicit structure. For example
- do not free things at all, just reserve a big chunk of address space and let the OS populate it as needed. When the process quits the OS frees everything automatically.
- do the same thing for parts of the program but implement the "OS part" in the program itself. There are variations of this known by terms such as "memory arena" or "pools". Basically, just take care to group allocations by end of lifetime. Then you can free everything in one go without tracking each lifetime individually in a stack frame (which is insane).
Really, as an end-user the issue with exceptions in C++ is another:
a) it's impossible to figure out what throws by looking at code.
b) it's (nearly) impossible to ensure that something doesn't throw
This means on one hand that one has to assume that any code can throw and manage resources appropriately, which is by now known and there are well-established idioms around it. On the other hand though it also means that the silliest error from a tiny library can bubble up into the event loop/main function and terminate an application.
Swift's syntax for exceptions illustrates what I mean, even though Swift does not unwind the stack.
I thought the not-thrown case was pretty much zero-cost, at least in newer versions of Clang... How much of a slow down are we talking here?
If you want your code to be fast, use 'nothrow' everywhere.
I don't know about newer versions of Clang, but I recall Chandler Carruth mentioning that LLVM abandons much optimization across EH blocks as infeasible.
I use that a lot in constexpr computations -- to stop the compilation, I usually do 'throw __LINE__'.
-- Using a more complex type is not warranted -- there is no catching end in constexpr.
-- And in case the same routine ends up called non-constexpr, it will be easy to identify the place that called 'throw' -- line numbers are unique without additional effort. Just don't put two throws on the same line.
Why yes I just love to get a "Error one of the billion files this application tried to load wasn't available ErrorCode: ERR_MISSING_FILE_FUCK_WHO_KNOWS_WHICH". What I like about exceptions is that they make information that can't be encoded in a 32 bit integer value available to top level error handlers.
Assuming not all code you use is your own, how does this work in combination with other code (like the STL) which is not nothrow?
And this remain true: a lot of things can fail (arithmetic operations, every IO, etc) so the error system must be very "lean" otherwise the "happy path" is drowned in the error propagatio/handling code..
[Why?] https://gigamonkeys.com/book/introduction-why-lisp.html
[Exceptions] https://gigamonkeys.com/book/beyond-exception-handling-condi...
I could image throwing an int when writing a shell utility and throwing the return value of main as an int but I suppose doing that usefully would be pretty rare. Usually one cares more about whether a shell utility is successful or not not so much about the precise reason it failed. So, while I could imagine doing that, I don't see myself going for that option too likely.
When an integer is the only value I need in the catch handler, I sometimes throw them, but only negative integers.
https://docs.microsoft.com/en-us/openspecs/windows_protocols...
Not sure if you consider this legitimate, but I have seen code that throws an errno.
Consider the case of running out of memory. One option is to pre-allocate all the memory the algorithm will need, then it can't run out of memory. Another option is to regard out-of-memory as a fatal error, not one that needs to be thrown and caught.
Another example is UTF-8 processing. Early on, I did the obvious when invalid UTF-8 sequences were discovered - throw an exception. But this got in the way of high speed string processing (exceptions, even in the happy path, are slow). But what does one do anyway with such input? abort the display of the text? Nope. The bad sequence gets replaced with the Unicode "replacement character". This turns out to be common practice, and now my UTF-8 processing code cannot fail! And it's smaller and faster, too.
It's a fun challenge to figure out how to organize the program so it can't fail.
As I mentioned in another comment, I've had good success in the trenches with the poisoning technique.
This is an interesting tidbit that cost me a week of debugging recently - a try/catch block at the top of the call stack wasn't catching an exception.
We set up an exception handler that calls main in a try/catch block, so that any thrown exceptions can be caught, processed, and dispatched to our crash-logging system.
But destructors are marked nothrow by default. So we had a case where an object was destroyed, and about 10 levels down from its destructor some other system threw an exception, intending it to be caught by the top-level catch block.
But during stack unwinding we passed through the nothrow destructor and std::terminate got called before unwinding got to the top-level try/catch.
Second, the std::terminate call doesn't get called from the dtor, it gets called from the stdc runtime in the call frame of the throw (with OS code in between). The stack isn't actually unwound at this point, it's more like the stdc runtime is walking up the call stack looking for a landing pad at each frame.
Third, I didn't know about how this all worked, so I was trying to piece it all together for the first time.
Yes, I saw the throw happen. But the symptom was then that the program just... terminated.
Also note the ABI for C++ exceptions followed by G++ et al is actually documented as part of the Itanium C++ ABI :
IIRC Ian LAnce Taylor had a series of blog posts that did shed light on a lot of these details.
At the bottom layer you have the unwind API. This is actually generally defined by the platform-specific ABI, although the definition here on most Unixes is "we have a .eh_frame section that's basically the same as .debug_frame in DWARF." The API is provided by some platform library (libunwind), and it's language-agnostic.
Part of the generic unwind structure is that each stack frame has a personality function and a Language-Specific Data Area, and the unwind semantics will call the personality function to figure out whether to drop off into the stack frame, and where in the function to drop off, or continue unwinding the stack. The personality function itself uses the LSDA to figure out what to do. The personality function lives in the language standard library (e.g., libstdc++), and the LSDA format is of course entirely undocumented (i.e., read Ian Lance Taylor's blog posts as the best documentation that exists).
The final level of the ABI is how you actually represent the exceptions themselves. This is provided by the Itanium ABI and describes the names of the functions needed to effect exception handling (e.g., __cxa_begin_catch), the structure layouts involved, and even some nominal data on how to handle foreign exceptions which don't really exist.
And that's not entirely true for all systems. ARM notably uses a different ABI exception handling, which is rather close to the standard Itanium exception handling except the details are different. Some operating systems choose to forgo unwinding-based exceptions in lieu of setjmp/longjmp as the standard ABI, which of course requires different versions of everything. And Windows has an entirely different form of exception handling which isn't centered around unwinding but actually calling exception handlers as new functions on the stack, requiring frame links to the original-would-be-unwound-to function.
This is in practice almost invariably the case for large programs. Somebody (Herb Sutter maybe?) asked the major C++ Standard library implementers, and none of them really bothers to handle the tricky parts of this. If you write code to try to pre-allocate a 10TB vector of 'Z's you can probably get that to throw you the exception that you read about in the documentation, but if the library code for opening a file can't find 64 bytes for a temporary object they aren't going to bubble up an exception, they're going to crash your program and too bad.
If you write an operating system kernel, you care about running out of memory, if you write the embedded firmware for a jet engine, you care (actually you likely never allocate memory at runtime, so in that sense you don't care), but in both those cases you live in a world where many other problems are far above you out of sight, so you can afford to care about stuff like how much RAM there actually is. You don't want the C++ standard library down where you live, and they don't want your problems. Everybody who lives up above the C++ standard library doesn't care, which is why the people implementing the library don't care either.
Yes, all of Unicode processing should use U+FFFD (the replacement character). Not just UTF-8, if you have any reason to do anything Unicode related and you're in a state where other paths forward are nonsense, emit U+FFFD. Take XML. Because the people involved hated ASCII control codes XML says you can't express them in XML 1.0 (which you will in practice have to use). I don't mean they need to be escaped, I mean you intentionally cannot express them. So if you have some arbitrary ASCII text that might include control codes, you can't write that as valid XML. What to do? Emit U+FFFD whenever this problem arises. Your users go "Huh, my Vertical Tab turned into this weird character in the XML output" and you send them to talk to the XML committee which will tell them they're a sinner and must repent of the evil of Vertical Tab and now your user knows you aren't crazy and maybe they stop using XML or maybe they don't but either way your code works.
what I mean is that, if you run that in a debugger, you're going to see something akin to this when the crash occurs (and have your ide stop exactly where the offending exception was thrown ; I don't even have to set `catch throw` for this to work): https://ibb.co/hK3skvz - at least on windows, mac, linux. What platform are you running that does not support gdb at all ? pretty much anything that isn't a PIC16F or Z80 supports it..
Every implementation has to do something to actually be standard C, and this is an example. It’s rather similar on other hardware, but as far as the standard goes that is irrelevant.
There's also potential operating system involvement that's not really covered in this article.
While we can benefit from #[no_std] crates on crates.io, unfortunately we can't use any crates that require standard because the standard library does not propagate errors properly, so we maintain our own implementation of most of the standard library for Linux only, that propagates all errors using Result..
It's a huge pain point, but at least Rust allows us to do it.
Is "panicking on allocation failure" the only example of this, or are there others?
https://github.com/castel/libcastel/blob/master/runtime/sour...
https://github.com/castel/libcastel/blob/master/runtime/sour...
I remember that at the time there were very few resources on personality functions, even in the LLVM doc - I had to make a lot of research before finding your articles, which were extremely helpful!
I got reminded of them yesterday after someone pinged me on a Stack Overflow answer I made at the time, asking for an updated link; after I found your long-form article I figured it would be a good topic for HN as well :)
https://stackoverflow.com/questions/16597350/what-is-an-exce...
I tried to set http301s but gave up when I had to pay for it :)
template<typename Fn>
auto CallWithSupervision(Fn fn) -> decltype(fn()) {
// supervision loop
// configure conditions as needed
while(true) {
try {
return fn();
}
catch(std::exception& e) {
// log failure details
}
catch(...) {
// optional: exceptions out of handled set?
// kill supervisor.
throw;
}
}
}
//elsewhere
CallWithSupervision([relevant=state,&nd=captures]() { return Client(relevant, nd); });
Modify as you see fit. It's the simplest, synchronous Erlang supervisor, in C++. And it will already work with your code - exception handling is very composable this way.Exceptions occur when "Very Bad No Good Undefined Horrible Things" have (at last, been found to have) happened.
So if you have no guarantee that the memory space is uncorrupted, you have no guarantee that all resources have been recovered, you have no guarantee that in attempting to deallocated resources that they have been allocated in the first place. (Read the fine fine print on exceptions in constructors and destructors.) TL;DR; Don't do that. But exceptions are what happen when people do what they shouldn't.
Go try this with std::sort (or std::adjacent_find or whatever) and tell me which of their implementations you had to modify.
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.
> 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.
.. 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 disagree :).
Exceptions are fundamentally equivalent to "return sum type" error handling pattern. In an exception-enabled environment, you can imagine every function returning some Foo is really returning a {Foo, Error}. Then, every call like below, outside of try/catch block:
value = SomeFunction();
is secretly translated to: maybeValue = SomeFunction();
if(!maybeValue) { return maybeValue.error(); }
You can devise analogous translations for code in try/catch blocks.The fundamental difference between exceptions and an Expected/Maybe mechanism is that exceptions don't force you to be explicit about all those Maybe values. If you want to handle some Error three layers up in the call stack, you don't have to litter the intermediary layers with explicit Maybes everywhere.
(This is, unfortunately, also their drawback in typical implementations - the set of possible Error types in the hidden Maybes becomes effectively open-ended, when with explicit Maybes, it's constrained and visible in source code - and, perhaps more importantly, in the ABI.)
The other day I did an experiment - I wrote two equivalent pieces of nontrivial production code, one using C++ exceptions, and other using the tl::expected library. If you looked past the syntactic noise, they mapped almost 1:1 in terms of error handling and error recovery patterns.
I argue that as so very often C++ the defaults are wrong. You can easily do the wrong thing, or you can go to a lot of effort to do the right thing, and since the right thing was technically possible C++ practitioners proudly declare C++ got this correct, and I say it did not.
[See also: everything about const from West Const being endorsed in NL26 despite being silly, through to the fact that the default is mutable for no good reason; the fact char isn't necessarily signed or unsigned you need to pick one if you care; need to explicitly use a provided replacement for the array type because the default built-in array type is broken; the default meaning of the literal "Hello, world" is this awful NUL-terminated byte array using that broken default array type; Way too many dubious implicit coercions, including narrowing conversions everywhere; I could go on]
Because we're not handling truly exceptional cases we will often want to treat the OK and error cases similarly. We tried to go outside and it was raining so maybe we should get an umbrella before venturing out again, but it wasn't on fire out there so we don't need to freak out and abandon our remaining plans to flee the fire immediately.
Exceptions make this needlessly difficult whereas sum types don't. The exception deliberately changes program execution, that is in fact its purpose, whereas the sum type lets you carry around the error and its context just as you would an "OK" result, until you need it for something or you decide you didn't need it and drop it on the floor.
Bad defaults get replicated for consistency. There are a lot of bad practices -- things you definitely shouldn't do -- that are now enshrined permanently in the ABI of the C++ standard library and so for consistency you're going to inherit those practices.
As a result I agree that Expected doesn't feel nicer in C++ today than exceptions but I argue that's a language defect, in a better language you'd find Expected worked better for the unhappy paths of your program and exceptions remained available for those truly exceptional cases that the programmer did not anticipate happening. Now, one programmer might feel that even "File already exists" truly is exceptional for their scenario, while another considers "Disk I/O error" to be merely an error they can cope with and no big deal (maybe the second programmer is writing an IT forensics program). That's going to vary, but the way for a standard library to reflect that is to use Expected almost everywhere and allow the developer who thinks "File already exists" is exceptional to throw for it, not have the standard library throw everything and then you race around trying to catch what you need to and hope you didn't miss anything.
I find exceptions to be a perfectly fine error handling mechanism.
I certainly prefer it to cluttering my code with explicit checks for return codes and such like.
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.
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...
while also encoding into the type system that it can fail/what failure modes there are, while also forcing you to handle it locally.
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.
Local handling was meant in terms of locally seeing pitential errors
All of that is to say that Java try blocks do not have any direct dynamic cost when exceptions are not thrown.
Even throwing exceptions and catching them locally in Java can be fast. If everything gets inlined into one compilation unit and the exception object is escape analyzed, HotSpot will absolutely just emit a jump.
Not that I disagree with your overall point--Virgil just doesn't have exceptions--but from your description here it just sounds like your compiler is far behind the state of the art in terms of optimizing exception-heavy code.
As for clang, see what Chandler said. But maybe things have changed in the last couple years.
[1] for example, Java doesn't have objects on the stack that need their destructors run. That's a massive simplification.
Most of the lost optimization opportunities are second-order costs, not first-order costs. Java JITs make up for the extra flow edges by focusing more on global optimizations rather than local (e.g. GVN vs LVN, global code motion, global flow-sensitive load/store elimination), etc. Generally a possible exception edge splitting a basic block doesn't hurt because the non-exceptional control flow will still benefit from flow-sensitive optimizations (i.e. it has only one predecessor anyway).
We're splitting hairs anyway. Like I said, Java JITs are significantly more advanced at optimizing exception-heavy code. I'd be really surprised if you saw anything more than a 1% increase in performance, actually, no, scratch that. I doubt you can even reliably any speedup distinguishable from noise from just disabling all support for exceptions in most Java code, unless you are talking about metadata. Top-tier JITs really are that good.
I'm sorry, but this is just not true of at least the Itanium exception handling system used by Linux, macOS, etc on most architectures.
It does make the exception handling data quite large and throwing extra slow, of course.
int! a = getMayError();
// a is now either an int, or contains an error value.
// foo(a) is only conditionally invoked.
int! b = foo(a);
// The above works as if it was written:
// int! b = "if a has error" ? "the error of a" : foo("real value of a");
// A single if-catch with return will implicitly unwrap:
if (catch err = b) {
/* handle errors */
return;
}
/* b is unwrapped implicitly from here on and is treated as int */
if (try int x = a) {
/* conditionally execute if a isn't an error */
}
I think this is kind of formalizing the poison technique but external from the call (that is, "foo" does not need to know about "failables" or return one, the call is skipped on the caller side). Here are some more examples: http://www.c3-lang.org/errorhandling/I'd be interested in hearing what you think about this (experimental) solution Walter.
Right x >>= f = Right (f x) -- normal case
Left y >>= f = Left y -- error propagation case
(The slogan is “monadic bind is an overload for the semicolon”.)I don’t expect this knowledge will dramatically change what you’re doing, but now that you know that’s how some people call it you have one more place to steal ideas from :)
What I do is have the error reported at the source, and then return the poisoned object. A better way would possibly be put the error message in the poisoned object, and report the error somewhere up the call stack.
typedef struct
{
err_t error;
int error_line;
char *error_msg;
...
...
} thing_t;
// set out of range error
thing->error = THING_ERROR_OOR;
thing->error_line = __LINE__;
thing->error_msg = "outofrange"
You can grep on 'outofrange' and find where the error was set.I originally started doing that to mark 'bad' analog readings in process control equipment. I wrote my filters and control loops to be able to 'eat' occasional bad readings without barfing. Worked very well.
if( nullptr != pfnErrorSink )
pfnErrorSink( "outofrange", __FILE__, __LINE__ );
return E_BOUNDS; // Or sometimes throw E_BOUNDS;
Where pfnErrorSink is either global, thread_local or a field keeping C function pointer provided by whoever consumes the code.That's up to you. You can do it as:
1. return a poisoned value
2. return a safe value, like `0` for its size
3. treat it as a programming bug, and assert fail
4. I know `null` is hated, but it is the ultimate poisoned value. Try to call a method on it, and you are rewarded with a seg fault, which can be considered an assert fail.
5. design your poisoned object to be a real object, with working methods and all. It can be the same as the object's default initialized state.
In other words, it's necessary to think about what the poisoned state means for your use case. I use all those methods as appropriate.
(Not necessarily relevant or correct thoughts:
- Your language still seems to mark potentially-failed values in the type system, even if it writes them T! not Either Error T or Result<Error, T>;
- The way Haskell’s do-notation [apparently implemented as a macro package in Rust] is centred around name binding seems very close to what you’re doing, although it [being monadic, not applicative] insists on sequencing everything, so fails the whole block immediately once an error value occurs;
- Of course, transparently morphing a T-or-error into a T after a check for an error either needs to be built into the language or requires a much stronger type system; Haskell circumvents this by saying that x <- ... either gives you a genuine T or returns failure immediately, which is indeed not quite what you’re doing.)
Here is an example:
int! x = ...
int*! y = &x;
int**! z = &y;
// If it had been a type then
// int!* y = &x;
// int!** z = &y;
// int*! y = &x;
// means
// int*! y = "if x is err" ? "error of x"
// : "the address holding the int of x"
This also means that `int!` cannot ever be a parameter, nor a type of a member inside of a struct or union.The underlying implementation is basically that for a variable `int! x` what is actually stored is:
// int! x;
int x$real;
ErrCode x$err;
// int*! y;
int* y$real;
ErrCode y$err;
// y = &x;
if (x$err) {
y$err = x$err;
} else {
y$real = &x$real;
}
int z;
// y = &z;
y$err = 0;
y$real = &x;
The semantics resulting from this is different from if `int!` had been something like struct IntErr {
bool is_err_tag;
union {
int value;
ErrCode error;
};
};
Which is what a Result based solution would work like. In such a solution: int! x ... ;
int!* y = &x; // Ok
int z = ...
y = &z; // <- Type error!Allocating local variables into registers rather than assigning stack locations for them. Registers are faster than memory. EH unwinders restore the stack before jumping to the catch block, but not the register contents.
Stack maps wouldn't be necessary for non-pointers, like an integer variable. Stack maps also have their own performance problems, which is why D doesn't use them.
> Registers are faster than memory. EH unwinders restore the stack before jumping to the catch block, but not the register contents.
I get that, which is why Java JITs don't use callee-saved registers. I mean, they use all the physical registers, of course, but their calling convention does not have callee-saved registers.