Java and Rust are the only language that I know of that has proper handling of exceptions; mandatory declaration as part of method declaration, since exceptions ARE an integral part of the contract. (Yes, I consider the Result<T, E> being corresponding to exception declaration, since the return value MUST be checked prior to use of T.)
Honestly, I thought the diatribe would focus on needless complexity.
The starting example is how I'd do it in C:
```
void f(const char* p) // unsafe, naive use
{
FILE \*f = fopen(p, "r"); // acquire
// use f
fclose(f); // release
}
```
Wouldn't the simpler solution be ensuring your function doesn't exit before release? All that c++ destroyer stuff appears somewhat unnecessary and as the author points out, creates even more problems.
It's a solution that addresses a fundamental problem in C: that it's often baroquely complex to do that in C, and incredibly easy to make mistakes. It is possible, but it is very often not at all simple. Thanks, but no thanks.
Author completely misunderstands how to use exceptions and is just bashing them. A lot of what he says is inaccurate if not outwardly incorrect.
Also Bjarne's control of C++ is quite limited, and he is semi-retired, so asking him to "fix his language" is fairly misguided. It's designed by a committee of 200+ people.
Anyway what you want seems to be to not use exceptions, but monads instead. These are also part of the standard, it's called std::expected.
>The first is that our error message may not be correct. It’s possible that the exception we’ve caught was not introduced by opening this file, and, the errno may not reflect the errno at the time fopen was called.
All that is needed is a better File_error type that includes the error that happened.
The first two problems can be solved in a straightforward way with more custom exception types. For the "bigger problem", catch(...) can be used to prevent your code from crashing. If you really want to handle each case explicitly you could also use enums in combination with compiler flags that enable exhaustive checking.
I avoid using exceptions myself so I wouldn't be surprised if I misunderstand them :) I love to learn and welcome new knowledge and/or correction of misunderstandings if you have them.
I'll add that inspiration for the article came about because It was striking to me how Bjarne's example which was suppose to show a better way to manage resources introduced so many issues. The blog post goes over those issues and talks about possible solutions, all of which aren't great. I think however these problems with exceptions don't manifest into bigger issues because programmers just kinda learn to avoid exceptions. So, the post was trying to go into why we avoid them.
I didn’t really understand the writer’s comments with exceptions and I don’t code in C++.
Their main complaint about exceptions seems to be that you can’t handle all of them and that you don’t know which you’ll get? If we compare this to python, what’s the difference here? It looks like it works the same here as in python; you catch and handle some exceptions, and others that you miss will crash your program (unless you catch the base class). Is there something special about C++ that makes it work differently, or would the author have similar problems with python?
I agree with the other comments that this understanding of exceptions is wrong. He's not wrong about these two points:
- Correctness: you don’t know if the exception type you’ve caught matches what the code throws
- Exhaustiveness: you don’t know if you’ve caught all exceptions the code can throw
But that's actually not a problem. Most of the time you shouldn't catch a specific exception (so always correct) and if you are catching then you should catch them all (exhaustive). A better solution is merely:
That's all you need. But actually this code is also bad because this function f() shouldn't have a try/catch in it that prints an error message. That's a job for a function (possibly main()) further up the call stack.
My biggest beef with exceptions is invisible code flow. Add the attribute throws<t> to function signature (so that its visible) and enforce handling by generating compiler error if ignored. Bubbling up errors is OK. In essence, this is result<t,e>. Thats OK. Even for constructors.
What I dislike is having a mechanism to skip 10 layers of bubbling deep inside call stack by a “mega” throw of type <n> which none of the layers know about. Other than
Don't think of the uncaught type as a "mega" throw. It's just a distinct type of error that nobody specified that they can handle. If you truly worry about the caller missing something, then somewhere in there you can catch anything and translate into a recognizable exception. This is easiest to understand in a library. The interface functions can catch all and translate into one particular exception type for "unknown" or generic errors. Then, that will be caught by anyone using the thing as documented. This only works if it's just reporting a non-fatal error. In case of a fatal error, it can't be handled, so the translation is kind of pointless.
All this hassle can be avoided by using `cleanup` compiler attribute.
Manage classical C resources by auto-cleanup variables and do error-handling the normal way. If everything is OK, pass the ownership of these resources from auto-cleanup variables to C++ ctor.
Note this approach plays nicely with C++ exception, and will enter C standard in the form of `defer`.
Or do without exceptions for control flow. For example Elixir does have try/catch but it's used very rarely because most functions return tuples with the first element being either :ok or :error. Then we can pattern match it and return an :error to the caller if we have to, possibly bubbling up several levels return after return. Or let the process crash and restart while the rest of the application keeps running. A surprising number of errors eventually fix themselves (API calls, disk space, missing data) when you design the system to attempt more times to complete its tasks. That's not only a characteristic of Elixir and the BEAM languages. You can do it more or less easily on any language. Maybe you need a queue and workers reading from the queue and all it takes to manage them, and BEAM makes it convenient by including most of it.
Or, if you don't want to go to the trouble of writing an RAII wrapper class for FILE*, just use scope_guard and (after determining that fopen() succeeded) register a lambda to close the FILE* on exiting the function (including by throwing an exception). I'm not a huge fan of scope_guard (or defer() in other languages), but it gets the job done for one-off cases.
I think i agree with commenters that article kind-of uses exceptions wrong.
But this shows problems with C++ exceptions, C++ codebases are literred with bad exception usage because "normal" programmers don't get it. Most of didn't get it for long time. So, they are complex enough that their usage is risk.
Anyway, IMO C++ exceptions have two fundametal problems:
* lack of stacktrace, so actually lack of _debugabbility_, that's why people try/catch everything
* destructor problem (that actually there are exceptions tht cannot be propagated further and are lost
You probably do want that exception to bubble up, actually. You probably don't want to catch it immediately after open. Because you need to communicate a failure mode to your caller, and what are you going to do then? Throw another exception? Fall back to error codes? Unwind manually with error codes all the way up? And if so, logging was the wrong thing to do, since the caller is probably going to log as well, based on the same philosophy, and you're going to get loads of error messages for one failure mode, and no stack trace (yes, there are ways of getting semi-decent stack traces from C++ exceptions).
Exception safety has a lot of problems in C++, but it's mostly around allowing various implicit operations to throw (copies, assignments, destructors on temporaries and so forth). And that does come down to poor design of C++.
This strikes me as all wrong. The whole point of exceptions is control flow and destructors. By getting rid of RAII for the sake of simplifying the callsite a little, the author fails to obtain the real advantage, which is automatic resource unwinding of all local resources under failure conditions both during initialization and usage.
If you want to simplify the callsite, just move the exception handling to a higher scope. I can admit it’s a little irritating to put a try/catch in main but it’s trivial to automate, and most programs are not written inline in main.
The main problems I see with destructors have to do with hidden control flow and hidden type information. That said, hiding exceptional control flow from the mainline control flow of a function is also a useful feature, and the “exception type” of any given function is the recursive sum of all the exception types of any functions or operators it calls, including for example allocation. That quickly becomes either an extremely large and awkward flat sum or an extremely large and awkward nested sum, with awkward cross-namespace inclusion, and it becomes part of the hard contract of your API. This means, transitively, that if any deep inner call needs to extend or break its error types, it must either propagate all the way up into all of your callers, or you must recover and merge that error into _your_ API.
For _most_ usecases, it is just simpler to implement a lean common interface such as std::exception and allow callers who care to look for more detailed information that you document. That said, there is a proposal (P3166 by Lewis Baker) for allowing functions to specify their exception set, including via deduction (auto):
If C++ had a contract on what exceptions a function can throw with compile time check to enforce caller catches those exceptions, would it make it better?
Guess Java does that, not much experience in Java here.
29 comments
[ 4.7 ms ] story [ 47.3 ms ] threadFor example, it plays poorly with generics. (Especially if you start doing FP, lambdas.)
If Java added union types, it wouldn't be a big deal, but AFAIK this is still a limitation.
The starting example is how I'd do it in C:
```
void f(const char* p) // unsafe, naive use
{
}```
Wouldn't the simpler solution be ensuring your function doesn't exit before release? All that c++ destroyer stuff appears somewhat unnecessary and as the author points out, creates even more problems.
https://yosefk.com/c++fqa/
Also Bjarne's control of C++ is quite limited, and he is semi-retired, so asking him to "fix his language" is fairly misguided. It's designed by a committee of 200+ people.
Anyway what you want seems to be to not use exceptions, but monads instead. These are also part of the standard, it's called std::expected.
All that is needed is a better File_error type that includes the error that happened.
And this is coming from someone that dislikes exceptions.
I'll add that inspiration for the article came about because It was striking to me how Bjarne's example which was suppose to show a better way to manage resources introduced so many issues. The blog post goes over those issues and talks about possible solutions, all of which aren't great. I think however these problems with exceptions don't manifest into bigger issues because programmers just kinda learn to avoid exceptions. So, the post was trying to go into why we avoid them.
Their main complaint about exceptions seems to be that you can’t handle all of them and that you don’t know which you’ll get? If we compare this to python, what’s the difference here? It looks like it works the same here as in python; you catch and handle some exceptions, and others that you miss will crash your program (unless you catch the base class). Is there something special about C++ that makes it work differently, or would the author have similar problems with python?
- Correctness: you don’t know if the exception type you’ve caught matches what the code throws
- Exhaustiveness: you don’t know if you’ve caught all exceptions the code can throw
But that's actually not a problem. Most of the time you shouldn't catch a specific exception (so always correct) and if you are catching then you should catch them all (exhaustive). A better solution is merely:
That's all you need. But actually this code is also bad because this function f() shouldn't have a try/catch in it that prints an error message. That's a job for a function (possibly main()) further up the call stack.What I dislike is having a mechanism to skip 10 layers of bubbling deep inside call stack by a “mega” throw of type <n> which none of the layers know about. Other than
But what I would really want is noexcept regions:
i.e. a way to mark regions of code that cannot deal with unwind and must only call non-throwing operations.Manage classical C resources by auto-cleanup variables and do error-handling the normal way. If everything is OK, pass the ownership of these resources from auto-cleanup variables to C++ ctor.
Note this approach plays nicely with C++ exception, and will enter C standard in the form of `defer`.
The page about try/catch explains it well https://hexdocs.pm/elixir/try-catch-and-rescue.html
But this shows problems with C++ exceptions, C++ codebases are literred with bad exception usage because "normal" programmers don't get it. Most of didn't get it for long time. So, they are complex enough that their usage is risk.
Anyway, IMO C++ exceptions have two fundametal problems:
* lack of stacktrace, so actually lack of _debugabbility_, that's why people try/catch everything
* destructor problem (that actually there are exceptions tht cannot be propagated further and are lost
Exception safety has a lot of problems in C++, but it's mostly around allowing various implicit operations to throw (copies, assignments, destructors on temporaries and so forth). And that does come down to poor design of C++.
If you want to simplify the callsite, just move the exception handling to a higher scope. I can admit it’s a little irritating to put a try/catch in main but it’s trivial to automate, and most programs are not written inline in main.
The main problems I see with destructors have to do with hidden control flow and hidden type information. That said, hiding exceptional control flow from the mainline control flow of a function is also a useful feature, and the “exception type” of any given function is the recursive sum of all the exception types of any functions or operators it calls, including for example allocation. That quickly becomes either an extremely large and awkward flat sum or an extremely large and awkward nested sum, with awkward cross-namespace inclusion, and it becomes part of the hard contract of your API. This means, transitively, that if any deep inner call needs to extend or break its error types, it must either propagate all the way up into all of your callers, or you must recover and merge that error into _your_ API.
For _most_ usecases, it is just simpler to implement a lean common interface such as std::exception and allow callers who care to look for more detailed information that you document. That said, there is a proposal (P3166 by Lewis Baker) for allowing functions to specify their exception set, including via deduction (auto):
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p31...
Is this an LLM prompt?
Guess Java does that, not much experience in Java here.