On 16/01/2018 03:53, Peter Dimov wrote:
This is circular reasoning. My specification would return success when the function succeeds. How would it return success is determined by what is the established way to return success. The current de-facto standard way of returning success is zero, so I'd specify my function to return that. Were the standard way to signal success something else, my specification would reflect that something else.
Actually there are several competing "standards" for return values in general. Functions that can only ever return success or failure tend to return 1 for success and 0 for failure (as booleans; literally these values in the case of C APIs but preferably as bool-typed values in C++). Functions that can return one kind of success and several kinds of failure tend to standardise on 0 == success, anything else == failure. (Usually inspired by shell conventions.) Occasionally this convention is abused by having some of the kinds of failure actually a qualified success, for things like "you already own that mutex" or "the file you were trying to delete didn't actually exist". Functions that are expected to return several kinds of success as well as several kinds of failure tend towards having >0 as success, <0 as failure, with 0 as an in-between value that could be either depending on context. Although implementations vary on whether the return value is actually signed, with -1 the first error code, or whether it's sign-magnitude, with 0x80000001 the first error code. Sometimes other high bits encode other special conditions like warnings or an error category. Outcome resolves the latter case by returning a success-or-error type that separately encodes each state, so that error_code itself doesn't have to worry about success values -- but even in that case it might still need to represent recoverable errors or partial-success warnings where there wasn't any success result to return instead. APIs written in the first style generally seem more "natural" (though as always that depends on what you're used to), but become problematic to change to the second style if they need to return multiple error states in the future (since the success convention is flipped). Most commonly this is done by punting and retaining the same return format but then making error information available out-of-band via a thread-local get-last-error call or similar. The whole 0==success argument goes away if the operator bool does call the error_category and asks it if the code is equivalent to {0, generic_category()}. (Related: why does this not have a std::errc enum?) It also begs the question of whether error_code() should be changed to be {0, generic_category()} rather than {0, system_category()}, as this would more unambiguously represent success. Of course, even that change would probably break user code, unless more things did error_condition compares.
These two ways to phrase it are the same; generic success and generic failure to succeed are complementary. Today, the logic is expressed in terms of if(ec) and if(!ec); whether the function tests for failure
do_f1( ec ); if( ec ) return;
do_f2( ec );
The problem with this code is that if it is ignorant of the underlying implementation (and only has a zero-success convention) it can *only* test for complete success or not. There's no way to generically handle partial success (or a recoverable failure), unless you can rely on the error_condition mapping, which in turn relies on the underlying library happening to implement that in a way that's compatible with your expectations. (Woe betide you if you want to switch from library A to library B but one of them forgot to implement a mapping, or elected to map to different codes.) And these things are important -- the primary advantage of error_codes over exceptions is exactly for handling the recoverable cases without invoking the exception handling machinery. Exceptions are invariably superior for handling unrecoverable errors. If you do remap the errors to your own domain as soon as possible then that simplifies things as only the code directly adjacent to the external library needs to worry about which error codes to track, and anything higher up only needs to deal with your own error codes and your error condition mappings. But this inevitably leads to a loss of information about the original error cause if the error does end up bubbling up outside the library (or otherwise reaching a generic logging function).