On Wed, Feb 10, 2016 at 5:02 PM, Gavin Lambert
On 11/02/2016 12:23, Emil Dotchevski wrote:
That's still a fuzzy line though. In the delete file case, the operation
of deleting the file cannot proceed because the file is already absent. However the post-condition of "the file no longer exists" is still met. Is that success or failure?
"Proceed", I mean the caller. Let's say you have code which opens the file then reads from it. The caller can not proceed to reading if the file couldn't be opened. So, the postcondition of the open operation is that the file was successfully opened. Similarly, the postcondition of delete_file is that the file does not exist, because presumably the caller of delete_file can't proceed if the file still exists.
As I said before though, there could be some applications (eg. certain types of databases) which can't proceed if that particular deletion action was not the cause of the file ceasing to exist (ie. if some other process deleted it in advance). It's not as clear-cut as you seem to be suggesting.
I didn't say that all possible functions from all possible libraries that may delete files should have the same postconditions.
If you know that the queue is never supposed to be full, that is, if the
full queue indicates a bug in your code, then you should assert rather than throw. Throwing is when you expect the program to successfully recover from an anticipated (by the programmer) failure.
The library can't know that, though (unless it was designed to be unbounded, so can't be full unless memory allocation fails), so it must pass this state out to the application to deal with, and an exception is *not necessarily* the right way to do so. (And neither is not-an-exception, for that matter.)
Here is an example: initializing a shared_ptr from an expired weak_ptr throws. The designer recognized that this is not always practical, so he gave us weak_ptr::lock, which does not throw. So the user has both options available. On the other hand, you might want dereferencing a shared_ptr to throw an exception in case it's empty, but that's not an option. Why? Because the shared_ptr designer said so. Dereferencing an empty shared_ptr is undefined behavior, and that's that. Designing correct preconditions and postconditions for each function isn't easy, and the answer isn't "oh I don't know, you might need 57 different versions".
If you *had* to pick one or the other, though, in cases where failure is unusual (such as failing to push to an unbounded queue) an exception makes more sense, and in cases where failure is not unusual (such as failing to push to a bounded queue) a return status makes more sense.
I think it's been well established that exceptions are great for dealing with unexpected failure (since you have to actively ignore them), but not so great for expected failure (since they can do heavy things like capturing call stacks and stack unwinding).
It depends. For example, if this is a keyboard buffer queue, then probably
you'd return a status to indicate that there's no more space, and the caller will "beep" to tell the user to slow down with the typing. In this case it would be annoying for the caller to have to catch an exception. On the other hand, if it's a job queue, where the caller expects all submitted jobs to be queued and at some point completed, then an exception is more appropriate, so the immediate caller wouldn't have to care (push won't return), while some context higher up the call stack can perhaps retry the whole sequence of jobs, or notify the user that the operation failed.
We can't discuss this stuff in the abstract. Defining correct postconditions depends very much on the specific API being designed.
Except that for things like generic data structures, the implementing library can't possibly know what the application is going to put in them or do with them.
So you have to deal in the abstract, which is why you often need both throwing and non-throwing API in those sorts of cases -- only at the application level can you finally decide which one to call.
The shared_ptr functions I just mentioned are counter-examples for the claim that only the application programmer can make these decisions. If you need another example, consider std::vector: if you attempt to push_back, and it needs to grow its buffer, but it fails to grow its buffer, it throws, even if the value_type has no-throw copy constructor. There is no no-throw option. Emil