5 ms·
The 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 erro
by Suncho 9y ago
The 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).
- aardvark179 9y agoHaven't checked CPython's extension API but Java's JNI does not allow exceptions to propagate through native code. When you call a Java method from JNI it will return and you have to check whether there is a pending exception, and do whatever is sensible in your native library. If a native library called from Java throws an exception that is not caught in that library then I think all bets are off - it probably depends on your language/platform ABI. The best case scenario would be that your Java stack gets nicely unwound in some way, but given it wouldn't be a Java exception object being thrown up the stack I'm really not sure what would happen. Unwinding the stack through native library or language boundaries is fraught with potential problems and language interop APIs have to be designed carefully round that sort of thing.
- hawski 9y agoI dislike exceptions, because they are magic. You can't tell what exceptions the piece of code you're calling will throw. It can also easily change with a library upgrade. I like exceptions, because they are magic. While prototyping or for small utilities it's perfectly OK to "crash" on any error - nice error message can help then. However I prefer when the final version handles all errors explicitly. I want to see it documented in code that error handling was not forgotten and it was deliberate to push certain errors back the stack. Of course it can work with documentation telling that this library never catches any errors, but it will put the burden on me. I also prefer to have a code, because documentation, more often then not, is lying. Having written all that, I'm wondering how exceptions work with Python's "Explicit is better than implicit". I guess in case of exceptions this rule has lower priority than some other rule that priorities simple looking code. That would align with my use of Python - strictly for one off utils and prototypes.
- Terr_ 9y ago> You can't tell what exceptions the piece of code you're calling will throw. Which is why I actually like Java's Checked Exceptions, at least for public-API purposes. It's really the exact same principle as having a contract for functions' return-values.
- _pmf_ 9y agoI like them, too. You can easily opt out of the "checked-ness" by wrapping it in a non-checked exception, so the argument that it pollutes your application is just a non-issue for me.
- AstralStorm 9y agoThis would have to work like the deprecated throw() statement and change the mangling scheme so that no linking errors would occur. I'm pretty sure it was proposed more than once in the standard committee.
- oselhn 9y agoI worked on huge old codebase. Mostly int return values were used as error indicator. But there were at least 3 non-compatible error enums (OK/ERR, PROCESSED/UNKNOWN/ERR, 1/0). Those enums partially used the same int values so you had to make sure to not mix them up (as you pointed documentation was frequently incorrect). Usually it took me several hours to check that error handling in some function is consistent or determine which of those enums is used. You can make huge mess with explicit error handling too.
- IshKebab 9y ago> If you're wrapping a try block around every piece of code that might throw an exception, then you're doing it wrong. Well then the C++ standard people are doing it wrong, because something as expected-to-fail as converting a string to a number throws an exception. What am I supposed to do if I want to load a text file containing numbers and print out where it failed? Yep, I have to wrap every string-to-number conversion in a try-catch block. Or in reality I make a wrapper function that does it. This is why exceptions are stupid - 80% of the time you want the context provided by the program counter in order to handle the error. int string_to_int(const std::string& s, int def) { try { size_t pos = 0; int v = std::stoi(s, &pos); // Check that the number was parsed from the entire string (stoi ignores trailing garbage). if (pos == s.size()) return v; } catch (std::logic_error&) { // This occurs if there is no conversion (e.g. no digits) or the number doesn't fit in an int. } return def; }
- qznc 9y agoWrite a proper lexer and parser. You are half-way there already.
- Suncho 9y ago> Well then the C++ standard people are doing it wrong, because something as expected-to-fail as converting a string to a number throws an exception. I wouldn't say that they are doing it wrong. They throw an exception because they don't want to make assumptions about how you would want to handle the error. What would you prefer the std::stoi() function to do?
- IshKebab 9y agoEither allow you to specify a default value on failure, or take an optional `bool* success = nullptr` parameter to indicate failure. Or in C++17, return `std::optional<int>`.
- Suncho 9y agoThen std::atoi() would be making the assumption that you want to handle the error as soon as the function call returns. Furthermore, if you don't check the "success" flag, then your function would fail silently. The exception ensures that something reasonable happens (program termination) when the client does nothing to handle the error. Exceptions are cleaner.
- IshKebab 9y ago> If you're wrapping a try block around every piece of code that might throw an exception, then you're doing it wrong. Well then the C++ standard people are doing it wrong, because something as expected-to-fail as converting a string to a number throws an exception. What am I supposed to do if I want to load a text file containing numbers and print out where it failed? Yep, I have to wrap every string-to-number conversion in a try-catch block. Or in reality I make a wrapper function that does it. This is why exceptions are stupid - 80% of the time you want the context provided by the program counter in order to handle the error. int string_to_int(const std::string& s, int def) { try { size_t pos = 0; int v = std::stoi(s, &pos); // Check that the number was parsed from the entire string (stoi ignores trailing garbage). if (pos == s.size()) return v; } catch (std::logic_error&) { // This occurs if there is no conversion (e.g. no digits) or the number doesn't fit in an int. } return def; } (A Rust-like `std::variant` return would be better here but we don't use C++17 yet in this codebase.)
- fnl 9y agoI only "partially agree": I often wrap entire sections of a program in a try block. This way, it can catch any unexpected errors, report them, and put the program back into a manageable state where it can keep running. So I think exceptions are a good thing for unexpected outcomes; Resource errors, programming errors, etc. The problem is, we use them in cases where the error is, technically speaking, a possible, or "expect-able", outcome (such as the classical example of de-serializing data). In those cases, I fully agree, a type-level, explicit handler using the Either monad would be better. I even think, the "expected case" is why Java introduced the notion of annotating exceptions. But as there are two different use-cases, this did not mix well...
- AstralStorm 9y agoOften, an error state machine is better than error code passing. (along the result or separately) Such a design forces you to handle all errors near the place they occurred without the syntactic overhead. (Threading problems are overrated. Easy to dodge by providing a Context attribute which then works like IO monad in Haskell.) It essentially works like exceptions without a forced stack unwind and return - which may or may not be good. Both are easy to convert and interoperate.