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.
Destructors are a higher-level and safer approach.
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.
I include function contracts as part of function declarations in headers. These take the form of macros that clearly define the function contract. The implementation of the function evaluates the preconditions at the start of the function, and is written with a single exit so the postconditions can be evaluated at the end of the function. Since this function contract is defined in the header, shadow functions can be written that simulate all possibilities of the function contract. The two are kept in sync because they both depend on the same header. This way, model checks can be written to focus on individual functions with any dependencies simulated by shadows.
The model checks are included in the same project, but are separate from the code under instrumentation, similar to how unit tests are commonly written. I include the shadow functions as an installation target for the library when it is installed in development mode, so that downstream projects can use existing shadow functions instead of writing their own.
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?
Regardless, in his example, he could achieve what he wants by wrapping the try in a lambda, and returning either the value from try or nullopt from catch. But clearly, that's just converting exceptions to another error-handling mechanism because he isn't doing it right.
He claimed that not handling an exception causes the program to crash, that's just plain incorrect. To be fair many people use the term "crash" liberally.
std::expected or equivalent is often used with std::error_code, which is an extensible system (error codes are arranged in categories) that among others interops with errno.
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...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;
}
}And this is coming from someone that dislikes exceptions.
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:
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.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
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
Flundstrom2•2h ago
nielsbot•2h ago
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:
Could be read as More here: https://github.com/swiftlang/swift/blob/main/docs/ErrorHandl...thomasmg•2h ago
mayoff•1h ago
https://github.com/swiftlang/swift-evolution/blob/main/propo...
I say limited because the compiler doesn't (yet, as of 6.2) perform typed throw inference for closures (a closure that throws is inferred to throw `any Error`). I have personally found this sufficiently limiting that I've given up using typed throws in the few places I want to, for now.
Someone•1h ago
Java experience taught us that, when writing an interface, it is common not to know the exception type. You often can’t know, for example, whether an implementation can time out (e.g. because it will make network calls) or will access a database (and thus can throw RollbackException). Consequently, when implementing an interface, it is common in Java to wrap exceptions in an exception of the type declared in the interface (https://wiki.c2.com/?ExceptionTunneling)
magicalhippo•1h ago
So as a user you could check for a generic file_not_found error, and if the underlying library uses http it could just pass on the 404 error_code with an http_category say, and your comparison would return true.
This allows you to handle very specific errors yet also allow users to handle errors in a more generic fashion in most cases.
[1]: https://www.boost.org/doc/libs/latest/libs/system/doc/html/s...
fakwandi_priv•2h ago
> if err != nil return err
mayoff•1h ago
mayoff•1h ago
munchler•2h ago
It's funny how often functional programming languages lead the way, but imperative languages end up with the credit.
JoshTriplett•1h ago
majormajor•2m ago
But they CAN be caught with a standard catch which may be non-intuitive if you don't know about them, and about that, in advance.
morshu9001•1h ago
wahern•1h ago
It's a shame. Were I designing a "low-level" or "systems" language, rather than put the cart before the horse and pick error results or exceptions, my litmus test would be how to make handling OOM as easy as possible. Any language construct that can make gracefully handling OOM convenient is almost by definition the fabled One True Way. And if you don't have a solution for OOM from the very beginning (whether designing a language or a project), it's just never gonna happen in a satisfactory way.
Lua does it--exceptions at the language level, and either setjmp/longjmp or C++ exceptions in the VM implementation. But for a strongly typed, statically compiled language, I'm not sure which strategy (or hybrid strategy) would be best.
morshu9001•1h ago
aw1621107•1h ago
I want to say the main driver for those right now is Rust for Linux since the "normal" panicking behavior is generally undesirable there.
MindSpunk•16m ago
Depending on the OS you likely won't even get the chance to handle the error because the allocation never fails, instead you just over commit and kill the process when physical memory gets exhausted.
I guess what I'm trying to say is designing your language's error handling primitives around OOM is probably not a good idea, even in a systems programming language, because it's a very rare class of generally unrecoverable error that only very carefully designed code can recover from.
Anyhow allocating isn't that absurd either. It keeps the size of Result<T, E> down by limiting the 'E' to be a pointer. Smaller Result<T, E> can help the compiler generate better code for passing the return value, and the expectation is the error path is rare so the cost of allocating might be outweighed by the lighter Result<T, E> benefits. Exceptions take a similar standpoint, throwing is very slow but the non-exceptional path can (theoretically) run without any performance cost.