8 ms·
C++ Error Handling: Why Use Eithers in Favor of Exceptions and Error-Codes
- makecheck 9y agoI'm not sure I follow. Assuming that lambdas can indeed be optimized/inlined, shouldn't the same performance gain exist with a single error-handling std::function (lambda) parameter in an otherwise-normal function? In other words, avoid wrapping the entire return value in a special type, avoid weird calling conventions, and simply supply a block to invoke if/when there is an error (and if there is no error, do nothing different).
- AstralStorm 9y agoIndeed, and it is generally cleaner. The functional equivalent is continuation passing style. However, it just pushes the problem one level up, so is not a full solution. It works well when combined with the some usual techniques like the error singletons and exceptions. You can also set an error code you need by capturing the result variable. Or just do what you need in the handler itself. Returning a special type is about as annoying as returning and handling an error code.
- m1el 9y agoOh, there's many problems with suggested approach. - Callback-oriented programming is its own deep can of worms. What do you want to inline where? In case of leftMap, it's easy for a compiler to inline flatMap, because it's small. What if you're providing a lambda to a huge function? What about variable captures? Really, we've been there with JavaScript. - It will either incur a yuuge runtime overhead or will require a new calling convention. - What if you want to transform your data multiple times? Will you use nested lambdas? IMO, using sum types is the best solution to this problem. Sum types are not "weird special type", they don't require "weird calling convention", C++ is weird for NOT supporting them.
- AstralStorm 9y agoWe have also been there in a variety of functional languages and no problems there. What runtime overhead, a function call? Pointer dereference? Even template magic on the error monad type won't save you from that one. The problem is shifted to the dereference. There you get either to handle an exception or error code, or explicitly check the state of the object. Nested lambdas do work reasonably well. But remember, were generally talking about error handling, not a callback that is supposed to run big transforms.
- hellofunk 9y agostd::function and lambdas are two very different things. std::function can store a lambda, but it is a memory-allocating library class, while a lambda is a language-level construct. Very important to understand the distinction between these two.
- AstralStorm 9y agoThere are improved template magic versions of std::function that do not inhibit inlining for stateless lambdas passed directly thanks to additional magic. At worst the overhead is a function call anyway and inhibition of inlining in the error path. Generally nothing to worry about.
- hellofunk 9y ago> There are improved template magic versions of std::function But not in the standard library. Of course, in C++, you can do anything if you venture outside the standard.
- slavik81 9y agoNo, performance would probably be worse. For one, std::function is not just for lambdas. It uses type-erasure and dynamic allocation to hold any sort of callable object/function. It's a generic type with a complex implementation and I wouldn't assume it would be optimized away. Additionally, when using Either the lambdas are in the caller and are invoked almost immediately by map. The scope is small and they do not enter into any other compilation unit. If you pass them into the called function, the compiler is only going to be able to optimize them if the called function can be inlined. Even then, the optimizer will have to be smarter, because the error handler will travel through a much larger scope and a type conversion.
- AstralStorm 9y agoEither inserts a conditional and union dereference. Unions are pretty terrible on their own to optimize, even with strict aliasing. The pointer dereference and call overhead is not big and error handling code is supposed to be cold anyway.
- shmerl 9y agoSo it's monadic error handling in C++? Looks quite Rust like as well.
- Kaali 9y agoMonadic error handling is quite nice for domain specific errors, but for exceptional situations which are not supposed to happen, you still need some sort of an exception system. And even with domain error conditions, exceptions has a nice property of saving a stack trace, which can make error hunting a bit simpler.
- dullgiulio 9y agoExceptional situations as in crashes, like writing to 0x0 address? Otherwise no, there is really no good reason for having some orthogonal value returning system that can jump up the stack until it's caught (if ever.) Many situations that are often considered exceptional are really not: cannot connect to server, no such file or directory, cannot bind to port...
- AstralStorm 9y agoAny situation that prevents functionality from working and should not happen in normal flow is exceptional. Including such connection failures or file open failures. They may have to be handled, but not at cost to the hot path. You cannot typically just "eat" such an error with default behaviour and expect whatever relied on it to work properly.
- stinos 9y agoAlso worth mentioning: LLVM's ErrorOr [1] which is like Either but with the right always an std::error_code. Rather convenient for APIs. Also a remark: the author uses left()/right() calls excplicitly to construct Either instances. This should not be needed (given Either has the proper constructors). One could argue it is clearer, on the other hand one could say it adds unnecessary visual noise. I'm obviously in the latter camp else I wouldn't make this remark :] [1] http://www.llvm.org/docs/doxygen/html/classllvm_1_1ErrorOr.html http://www.llvm.org/docs/doxygen/html/classllvm_1_1ErrorOr.h...
- Suncho 9y agoThe whole point of C++ exceptions is that they decouple the code that might raise exceptions from the code that handles them. If you know how to handle an error when it arises, then you shouldn't be using exceptions. If you're wrapping a try block around every piece of code that might throw an exception, then you're doing it wrong. The technique described in this article is not an alternative to exceptions... unless you were using exceptions wrong in the first place. EDIT: Replaced "any code" with "every piece of code."
- chuckdries 9y ago> If you're wrapping a try block around any code that might throw an exception, then you're doing it wrong. CS student here, can you elaborate on this? What code would you wrap in a try block if not code that could throw an exception? Are you suggesting try blocks should only be used to insulate function calls we know to be unsafe?
- ruleabidinguser 9y agoThe point is to handle exceptions in appropriate locations, and pass exceptions up if you cant handle them. I think theyre saying that throwing a try block around an exception just to avoid dealing with it is a problem
- Suncho 9y agoSorry. I said "any code," when I meant "every piece of code." Edited. C++ programs normally throw an exception when a C program would call exit() or abort(). So I would start by having one try block that surrounds your main() function with a catch-all block that prints some sort of error message before your program terminates. This is often all you need. If a situation arises in which you can handle an exception without terminating your program, you can throw a try block in there too. Exceptions automatically provide for you what you would write by hand in languages like C: // The C way int error_flag = EVERYTHING_IS_OK; int checked_div (int numerator, int denominator, int *quotient) { if (denominator == 0) return 1; *quotient = numerator / denominator; return 0; } int foo (int a, int b, int c) { int q; int err = checked_div (a, b, &q); if (err) { error_flag = FOO_DIV0; } return q+c; } * // The exception way (notice that there's no try block) int checked_div (int numerator, int denominator) { if (denominator == 0) throw runtime_error ("Divide by zero!"); return quotient; } int foo (int a, int b, int c) { return checked_div (a, b) + c; } Clearly, if you don't use built-in exceptions, you have to manually simulate the exception mechanism. The most popular strategies for simulating exceptions involve some combination of systematically returning and checking error codes or systematically setting and checking global error flags. I've demonstrated both above. The linked article is an example of the first strategy. Returning error codes prevents functions from directly returning their results. The "eithers" strategy is an attempt to make this a little smoother, but it doesn't solve the problem that it makes your code weird. Global error flags have their own host of problems. If you fail to check for errors, havoc will ensue. Furthermore, multithreaded environments turn global error flags into a tangled web of deceit. Nevertheless, these ad hoc strategies often suffice because the unusual nature of exceptional errors can prevent error handling code from clogging up your primary code path too badly. But even for simple situations, the compiler can do better than you can. Why add unnecessary complexity to your code? You should write try/catch blocks only when you know how to cope with exceptional errors. Needless to say, they should be pretty rare. But lets say that when a particular invocation of foo() fails, we want to use the maximum integer value as our result. Here's how it looks: // The C way int bar (int a, int b, int c) { int result = foo (a, b, c); if (error_flag != EVERYTHING_IS_OK) { assert (error_flag == FOO_DIV0); result = numeric_limits<int>::max(); error_flag = EVERYTHING_IS_OK; } return result; } * // The exception way int bar (int a, int b, int c) { try { return foo (a, b, c); } catch (runtime_error) return numeric_limits<int>::max(); } } Hmm... The exception version doesn't look that much simpler. After all, the assert() call will be compiled out of release builds. But C++ compilers can, and most modern ones do, generate code for the exception version as if it were written more like this: int bar (int a, int b, int c) { return foo (a, b, c); } // This is only called when a runtime_error exception is thrown. int bar_catch (int a, int b, int c) { // return to the same place bar() returns to return numeric_limits<int>::max(); } That's right. Even in the presence of a try/catch block, the code for foo is generated without the try/catch block. The error handling code gets stored somewhere else so it won't take up valuable cache space. Of course, what the compiler generates doesn't look exactly like my example, but the effect is the same. And this strategy works even for more complicated functions. The compiler creates a non-optimized version of the function. Then, when an exception gets caught, the non-optimized function can simulate the state to resume from. This exception handling strategy is often referred to as the zero-cost exception mechanism. The latest versions of Clang, GCC, and 64-bit VC++ all implement some variation of it. For compilers that don't, there's a cost to adding try/catch blocks to your code. But it's still cheaper than the cost of your own error checking. Even if it tried, I don't know if a compiler could possibly implement exceptions in a way that's slower than a manual error handling approach. There's a myth out there that exceptions add overhead or make the control flow of a program more difficult to reason about. Of course, this is the opposite of what they do. Even if you're working with C-like code that has no user-defined classes, switching to exceptions is a win. Exceptions enable some of the fundamental abstractions in C++. How else are you going to handle a memory allocation error in an implicitly-called copy constructor? If you'd like to get a sense of just how perversely you need to contort the C++ language in order to deal with the absence of exceptions, look no further than Google's C++ style guide. Google doesn't use exceptions in their C++ code. This policy has ripple effects throughout their guidelines. Among other things, they forbid constructors from allocating resources. https://google.github.io/styleguide/cppguide.html https://google.github.io/styleguide/cppguide.html Google's excuse for not using exceptions is that their existing exception-free code base is so large that it would take too much effort to convert it. But if they can interface C++ code with Java/Python/Go code, they should have no problem interfacing exception-free C++ with exception-full C++. I can't help but wonder if some of their engineers are confused about how exceptions work. Anyway, if you don't work for Google, use C++ exceptions. Wow. That turned into a rant (some of which was copy-pasted from something I wrote a while ago).
- workincog 9y agoAnd of course his "simple" example of an `Either` class leaks and doesn't call destructors, because C++ unions do not cleanly support types with nontrivial destructors. Meanwhile the open source project he links to has an implementation that is hundreds of lines of template magic that I gave up on interpreting. I've dealt with codebases like this and I strongly recommend against using this kind of arcane template incantations in production. It becomes an impenetrable fog for newbies, and a morass for veterans with something to prove.
- fnl 9y agoCan you please point out what about Buckaroo's (i.e., the author's) solution is wrong? I take it, his version of Either is copied from the neither project [1], so the destructor of the union is: ~Either() { if(isLeft) { leftValue.~L(); } else { rightValue.~R(); } } Which does explicitly call the destructors. (Obviously, if you only were referring to his eight-line toy version of Either for the blog, please forget my comment.) [EDIT: re-reading your comment once more, I see you did indeed mean the blog implementation; please ignore this comment.] [EDIT2: BTW, it seems the author is "affiliated" with that LoopPerfect/neither implementation.] [1] https://github.com/LoopPerfect/neither/blob/master/neither/include/either.hpp https://github.com/LoopPerfect/neither/blob/master/neither/i...
- fpoling 9y agoI have seen that using error singletones together with logging at the point of error worked rather nicely. Surely as a global state it is not thread safe, but when the state belongs to a component with well defined API and uses once an error, always an error strategy so error recovery requires constructing a new component, then thread-safety is trivial to address. A bonus of this approach is that error path through code is the same as for the non-error case. Thus getting good coverage for error cases in unit and integration tests is easier.
- ensiferum 9y agoI feel that the "error" handling is one of those "core things" that people have rarely fully understood. Especially newbies starting are often confused and things get muddled. In all honesty it took me +10 years myself to reach some kind of clarity on this. Anyway I find it that when you lack that clarity your code will be quite messy (isn't this always the case?) and things will get muddled. And since error handling is so precarious your implementation quality will suffer considerably. So below is my take on this with 3 clear labels and guidelines on how to apply. Hope this helps. There are 3 kinds of "errors". 1. Bugs. - created by programmer. - invalid state of the application - >it has transcended it's own logical realm and you can't reason about its behaviour anymore. - null pointers, OOB, etc. 2. Errors that are "expected" part of the program execution. - you need to write logic flow to deal with this - it's expected and quite normal that this might happen - incorrect (user) input, file not found, socket timed out etc. - this is really just an error from the end user's perspective. 3. Errors that are "unexpected". - some very unexpected error - system resource allocation failed, out of memory, out of file handles etc. - critical resource was not accessible (for example a "must have" config file was not found) How to deal with these? 1. Abort and dump core. Yep seriously, just do it. Blow up with a bang and leave a stack trace that you can analyze in the post-mortem debugger and see what went wrong. As a result your application will be simpler and more straightforward. Simply, don't try to write logic to deal with programmer failures. It will just clutter your program, mask the problem and make fixing it harder. int divide(int x, int y) { if (y == 0) { throw std::string("Divide by zero"); } return x / y; } passing y=0 the function is clearly a bug. It'd be much better to core dump here. Unless the function was designed with double purpose of validating the input and performing the actual function. I'd much rather split these into two different functions, since the core function may be called from different contexts, some of which might not require any input validation at all even. And the validation function clearly should not be using exceptions either since it's quite normal and expected that input from external sources (such as the user) can be malformed and you probably want to write logic that then bashes (sorry.. informs) the user about their wrong input. 2. Use error codes. 3. Use exceptions.
- fnl 9y agoAd #1: And bring down the whole process? Seriously, no, this is about the worse advice ever. Unless you are writing a tiny binary that works all on its own and on a single job/item, this is about as close as you can get to a mortal sin in any medium- or large-sized program. Figure out what the remaining viable state of the program is, report the error (yes, loudly!), and recover. This would be the far more correct advice for #1 (except for tiny binaries doing a single job only, as said).
- gbersac 9y agoI used to do a little c++ as a student and never found a way to correctly handle error using exceptions. I am now a scala user, and in scala Either is the canonical way to handle errors. I understood the Either construct quickly and after a year using it, I wouldn't come back to the c++ exception. Note that in c++, the absence of algebraic data type made it a little bit hard to understand how Either is coded.
- koja86 9y agoI am quite used to C++ exceptions and strongly favor them against error codes mainly because of error handling decoupling and the fact that exception (unlike return code) cannot be passively ignored - if you want to swallow it you need to do it rather explicitly. If you forget to handle exception it crashes loud and that I prefer. If anything I would be very please if we got advanced exception catchers in C++. Recently I drooled over Ada exception handling capabilities: https://en.wikibooks.org/wiki/Ada_Programming/Exceptions#Exception_handlers https://en.wikibooks.org/wiki/Ada_Programming/Exceptions#Exc...
- _pmf_ 9y agoWith exceptions, I can trivially pass error information through my whole call stack without any manual work and boilerplate. The whole syntax clutter / OMG-but-try-catch-is-so-ugly is a very stupid argument: with exceptions, you can opt out of local error handling and opt-in at a higher level in the call stack without losing any information. With return code / error codes, this is just not possible. So exceptions declutter application code to a high degree. Exceptions are to error codes as functions are to goto: there are valid cases where you want to prefer the latter, but they are few and far between.
- AstralStorm 9y agoIt is possible with global or contextual error state. This approach is used in iostream for example, and in a broken way in C library via errno. (Which does not work unless you check it very near.) Similar approach is often used in databases and file systems, marking an object as dirty or broken. Like the above, it is prone to ignoring errors and attempting to manipulate such object.
- partycoder 9y agoCheck out std::optional from C++17 (aka C++1z)
- MycroftJones 9y agoAfter a few years of using newLisp, I started using Eithers in my C and LISP code. Didn't know you called them that. I guess the idea has been "in the air" for a couple years now. Possibly people are being inspired by the multiple return values offered by Go. What surprised me about this link is that the author was writing C++, but using very Haskellish language and terminology. He even made the C++ code look somewhat like Haskell code. Wow.
- fetbaffe 9y agoOnce upon a time I hated exceptions, but today I like them more and more for each day. The hate was something I inherited from the early languages I worked with where exceptions where uncommon and was considered de facto wrong, it was the C approach of error handling. What I learned working on a medium-sized web project is that you have too few exception types. Yes, create more unique exceptions for each situation and avoid reusing them across the project. Too general exceptions make your code base suck hard. Complexity of exceptions with small benefit because you can't discriminate between them. ObjectNotFoundException is pretty useless but AccountNotFoundException is much better. Don't be afraid of types, use it to your advantage. Optimize the happy path and fail fast on the sad path. Note, have not tried this approach on a C++ project yet, maybe it will not work as well there.
- AstralStorm 9y agoWorks just as well until one of the types leaks accidentally by confusion. This is why you should have your exception types derive from common ones so that they can be reasonably caught by general handlers.