3 ms·
> dosomething(); > cleanup(); This scenario is contrived. Use RAII and you never have to worry about whether cleanup() is called. You can even write, wit
by quotemstr 7y ago
> dosomething();
> cleanup();
This scenario is contrived. Use RAII and you never have to worry about whether cleanup() is called. You can even write, with some macros, something like
FINALLY(cleanup());
dosomething();
And the right thing happens all the time automatically. But you shouldn't even have to use something like this, because cleanup should be implicit in the types you're using, e.g., unique_ptr.
> But the possibility that dosomething might throw an exception means that cleanupmight not get called.
You shouldn't care whether dosomething() throws: you should be using the language's scoped cleanup facilities to clean up on scope exit anyway, and doing that improves code clarity whether or not you use exceptions.
You're doing the wrong thing and then complaining that exceptions make code unclear. It's like complaining that a car doesn't float after you've driven off a dock.
Besides: exactly the same bug can occur in Rust, because panics. On the panic path, your cleanup() won't be called. And with "?", it's also easy to forget to call cleanup(), because "?" acts just like rethrowing an exception.
Exceptions, in practice, produce clear code. All the confusion scenarios people sometimes claim arise from the use of exceptions are just contrived.
- verdagon 7y agoQuestion about proper destructor/RAII use: C++'s unfortunately falls over when you throw an exception from a destructor, and also doesn't let us return error codes from destructors (not that any other language does). How do you deal with this exceptional destructor problem in practice? Or does it not come up enough to warrant concern?
- quotemstr 7y agoDestructors shouldn't fail: that's why they're implicitly noexcept in C++11. (You can, however, throw inside a destructor so long as you catch before returning.) In practice, it's not a problem. If you really want to do something fallible on every success path, you can use an IIFE or a named function to isolate everything before the thing you want to do on the success path. What cases do you have in mind?
- verdagon 7y agoI ran into something like this (very simplified): MyRAIIFile temp_file = ...; AddSomeStuffToFile(&temp_file); // threw something where MyRAIIFile has a destructor: ~MyRAIIFile() { underlying_file.close(); // also threw something } So, AddSomeStuffToFile threw an exception, and the destructor also threw an exception, meaning I had two exceptions in flight which was undefined behavior and caused some weirdness. It took many hours to track down this particular problem... I can see that one correct answer would be to put a try-catch around the .close() call, but that's the wrong place for that logic in my case; I want the caller of the destructor to decide what to do to recover. Even Java's checked exceptions would cause chaos here. Only returning an error in the destructor's return type (with a must-use annotation of course) would force me to handle this situation at compile time... but C++ can't do that. Any advice for this situation?
- zozbot234 7y agoThe usual advice is to add a TryClose method to your MyRAIIFile class that can signal failure, and that also keeps the object around in some well-defined state. This doesn't force you to handle the situation properly, but at least it makes it possible to do so.
- quotemstr 7y agoThe problem is that your close is fallible in the first place. In general, resource reclaim code should always be infallible. When you deallocate a resource, be it a file descriptor or a chunk of memory, you're returning something to the system. You're providing a gift. The kernel should never refuse this gift. Linux confuses the issue somewhat. close(2) can report errors, and I'm guessing that when your close() throws an error, it's just propagating something it got from the operating system. Thing is, close(2) errors aren't really errors. There are three cases: 1) close(2) succeeds; 2) close(2) fails with EBADF; and 3) close(2) fails with some other errors. In case #2, there's no problem. In case #2, your program has a logic bug and you should abort, not throw. In case #3, the close operation itself actually succeeded, and the kernel is just reporting some error that occurred during file writeback in the meantime. Errors in case #3 should be ignored. If you care about file durability, call fsync(2) before close. Catching and propagating IO errors from close(2) ensures nothing, since the kernel is allowed to defer potentially-failing IO operations until after the close!
- MaulingMonkey 7y ago> Besides: exactly the same bug can occur in Rust, because panics. On the panic path, your cleanup() won't be called. And with "?", it's also easy to forget to call cleanup(), because "?" acts just like rethrowing an exception. It's harder. If you're typing ?, you're aware that you're dealing with a result or option (with the values of the error case clearly documented in the type system), and aware of the fact that you might be returning early, and have forced your own function's signature to also be a result or option. In contrast, I've had code where finger->Position.X occasionally threw. Because a finger was released, invalidating the old finger ID, before it even gave you the chance to process the finger up event to realize you shouldn't query that finger. Did you know you need to wrap every finger position query in a try/catch? I didn't, so this was just a rare crash bug for awhile. What exception did I catch? I don't remember (but not a null reference nor an access violation exception), and it's not documented, so in new code I guess I'd just try { ... } catch (Platform::Exception^) {} despite the fact that we all know catch-all do-nothing statements are terrible. At least I can minimize the scope! For bonus points, the exception handling overhead was heavy enough that the framerate of the game we were working on would stutter if you pawed at the touch screen. Better than crashing, but still terrible. Not having source access to the throwing code, and touch release events being delayed, this was the least horrible option available though. Yaaaay. Was this a nasty edge case and not indicative of all use of exceptions? Yes. Do I eventually encounter such a nasty edge case in most, if not all, large scale projects involving exceptions in practice? Also yes. Often enough that it influences my preferred error handling mechanisms, even. Do I encounter such a nasty edge case in return based error handling? Not in the wild. The use of error codes makes it much clearer what can fail. On the rare occasion I've encounter something similar, it's been when harshly stress testing cross platform API abstractions in contrived tests, when disambiguating between multiple error codes. Occasionally, the underlying API does something strange and returns an unexpected error code (either a unique error code, or one of the usual error codes in unusual circumstances) and I take a suboptimal error handling path. Usually, at worst, the end user would've need to retry something, even if I hadn't caught and worked around the underlying API weirdness. > Exceptions, in practice, produce clear code. Do you catch NullReferenceException s instead of writing basic if conditionals to check values for null? Probably not. For such unexceptional cases it's less clear (which reference, exactly, was null?), and the performance overhead is often unacceptable. For more exceptional uses of exceptions, you can sometimes get clearer code. It's often brittle and mishandles rare edge cases, but it's clearer for the happy path at least. But I will happily sacrifice a little of that clarity to make that code handle the edge cases properly instead of crashing - because those exceptional cases aren't really all that exceptional after all.