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.
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:
<potentially throwing code>...
noexcept {
<only noexcept code here> ...
}
<potentially more throwing code>...
i.e. a way to mark regions of code that cannot deal with unwind and must only call non-throwing operations.That is, a `throw` statement in Swift simply returns an `Error` value to the caller via a special return path instead of the normal result.
More explicitly, a Swift function declared as:
func f() throws -> T {
}
Could be read as func f() -> (T|any Error) {
}
More here: https://github.com/swiftlang/swift/blob/main/docs/ErrorHandl...It's funny how often functional programming languages lead the way, but imperative languages end up with the credit.
This is why I think checked exceptions are a dreadful idea; that, and the misguided idea that you should catch exceptions.
Only code close to the exception, where it can see through the abstraction, and code far away from the exception, like a dispatch loop or request handler, where the failure mode is largely irrelevant beyond 4xx vs 5xx, should catch exceptions.
Annotating all the exceptions on the call graph in between is not only pointless, it breaks encapsulation.
For 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.
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.
class File_handle {
FILE *p;
public:
File_handle(const char *pp, const char *r) { p = fopen(pp, r); }
~File_handle() { if ( p ) fclose(p); }
FILE* file() const { return p; }
};
void f(string s)
{
File_handle fh { s, "r"};
if ( fh.file() == NULL ) {
fprintf(stderr, "failed to open file '%s', error=%d\n", p, errno);
return;
}
// use fh
}Do you mind to elaborate what you believe are the misunderstandings? Examples of incorrect/inaccurate statements and/or an article with better explanations of mentioned use cases would be helpful.
> it's called std::expected
How does std::expected play together with all other possible error handling schemas? Can I get unique ids for errors to record (error) traces along functions? What is the ABI of std::expected? Stable(ish) or is something planned, ideally to get something C compatible?
struct File_handle {
static auto Create(std::string const & pp, const char *r) -> std::expected<File_handle, std::error_code> {
auto p = fopen(pp.c_str(), r);
if (!p) return std::unexpected(error_code_from_errno(errno));
return File_handle{p};
}
~File_handle() { fclose(p); }
private:
File_handle(FILE * f) : p{f} {}
FILE *p;
};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++.
> and no stack trace
That loads of error messages, meaning every layer describes what it tried to do and what failed, IS a user readable variant of a stack trace. The user would be confused with a real stack trace, but nice error messages serve both the user and the developer.
try (File_handle fh {s, "r"}) {
// use fh
} unless (const File_error& e) {
// handle error
}
Where the "use fh" part is not covered by the exception handler. This is covered (in ML context) in https://www.microsoft.com/en-us/research/publication/excepti...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
The page about try/catch explains it well https://hexdocs.pm/elixir/try-catch-and-rescue.html
- 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:
void f(string s)
{
try {
File_handle fh { s, "r"};
// use fh
} catch (const std::exception& e) {
printf(stderr, e.what());
}
}
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.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...
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?
The third problem (RAISI) is a C++ specific problem that Python doesn't have. Partly because in Python try/catch doesn't introduce a new scope and also partly because Python tends not to need a lot of RAII because of the nature of interpreted languages.
I found this video a fascinating take on comparing C++ to Python if you haven't seen it: https://www.youtube.com/watch?v=9ZxtaccqyWA
I would expect yes. It is true, that in a lot of modern languages you need to live with that dynamism. But to people used to C, not knowing that the error handling is exhaustive, feels deeply uncomfortable.
All that is needed is a better File_error type that includes the error that happened.
void f(string s)
{
try {
File_handle fh { s, "r"};
// use fh
} catch (const File_error& e) {
fprintf(stderr, "File error: %s\n", e.msg.c_str();
return;
}
}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`.
Unfortunately, even the Linux kernel is no longer C because they use GCC compiler extensions (and are currently discussing adding MS ones too).
Guess Java does that, not much experience in Java here.
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.
As an aside, it is one of the reasons why I finally decided to let go of C++ after 20 years of use. It was just too difficult to teach developers all of the corner cases. Instead, I retooled my system programming around C with model checking to enforce resource management and function contracts. The code can be read just like this example and I can have guaranteed resource management that is enforced at build time by checking function contracts.
Is this an LLM prompt?