Boost logo

Boost :

Subject: Re: [boost] expected/result/etc
From: Emil Dotchevski (emildotchevski_at_[hidden])
Date: 2016-02-10 20:57:11


On Wed, Feb 10, 2016 at 5:02 PM, Gavin Lambert <gavinl_at_[hidden]>
wrote:

> 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


Boost list run by bdawes at acm.org, gregod at cs.rpi.edu, cpdaniel at pacbell.net, john at johnmaddock.co.uk