I've now read ALL the emails. Here's some thoughts, in no particular order.
- I wonder, Niall, if your uses of the empty state could/should be
compared to how people sometimes use -1 or 0, etc - ie "special values
as errors". I typically don't like that pattern (yet I've done it
anyhow - performance/laziness/etc - but typically only in localized
cases, not in "APIs"). So I'm wary about this empty state thing.
If we do have an empty state, I like it as a separate type - code
needs to deal with it separately. I like the idea of taking a
possibly-empty type, checking for empty, and creating a never-empty
type, and passing that along so future code need not worry.
- I think the narrow and wide contracts should be in the same type,
not separate types. This is C++.
I suggest "access" if you want long names. ie
T & val = oc.access_value(); // possible UB
(I almost also suggested "deref" but "deref_value" is wrong - you are
deref'ing the outcome, not the value)
- I agree with Vicente's concern that functions that behave
differently need to be named differently. I haven't looked at the new
(or old) API carefully enough, but as a (possibly hypothetical)
example:
wide_type::value() // returns value, or throw if no value
narrow_type::value() // returns value, or UB if no value
These functions are not the same, and should not be named the same.
Since you thus need different names anyhow (ie value and
access_value), why not put them both on the same API?
I'm not yet sure if you end up with similar examples with
possibly_empty_type::something() and never_empty_type::something().
- I see value in the idea that the narrow contract takes more typing,
but I actually *like* using * and -> for the narrow contract because I
have 30+ years of training in looking at * and -> and recognizing it
as "this may be unsafe, examine the code to see how it is guaranteed".
Boost.Optional specifically chose * for that very reason.
(on the other hand, I have 0 training on spotting access_value(), but
on the third hand, it is easy to grep for)
- exception() returns error, error() doesn't return exception, etc:
would it be better/easier/clearer if we introduced another name, such
as "failure" or whatever? ie exception() only returns the stored
exception, error() only returns the stored error(), and then
has_failure() would return true in either case, failure_exception()
returns exception, generates an exception from error if necessary,
etc?
- I dislike "exceptions are for exceptional circumstances". Yes it is
a common thing, but it is usually wrong. Using Outcome to enable the
pattern of "handle errors, but totally cancel the operation on
exceptions" might actually turn out to be a useful pattern, but I
think this is actually slightly different from the "exceptions are for
exceptional cases" camp, as they typically use that chant to choose
whether their API will use exceptions or errors, not both in a single
API.
So I agree with Emil - for most APIs, error-codes vs exceptions" is
basically trying to handle the same thing - failure (or
"disappointment").
Using both error-codes and exceptions in a single API is somewhat new.
And interesting.
(There is lots of code that almost-unknowingly uses error-codes and
exceptions in the same API, but that is because they don't realize
push_back throws or they pretend it doesn't, etc - it isn't really an
API design choice)
- this ring buffer thing sounds interesting. I haven't looked at it
at all, so maybe this makes no sense: should it use thread-local?
- I think std::expected should always throw something deriving from
std::exception. So if E derives from std::exception, we can throw it.
If E is exception_ptr we can (re)throw it. If E is
error_code/system_error/etc we can figure out what to throw. If E is
user-type, then we wrap it in bad_expected or whatever. (And/or allow
user E-to-exception customization somehow). This sounds complicated,
but it is easily summarized/taught as "the standard always throws
std::exceptions"
- I HATE the dual interface of std::filesystem. Worse, there is a
chance it is setting a precedent for future std APIs (dual APIs have
been proposed elsewhere). Let's please kill it before a dual API
strikes again.
However, the other problem with trying to "single-fy" the
std::filesystem (or any other) API is that users who want error codes
sometimes don't want the cost of the extra data (paths, etc). The
only way to handle that may be with customizable callback functions
that capture the error-info however you like, or something similar to
callbacks.
- I really look forward to Peter's never-empty variant - and have
been hoping for one ever since std::variant was finalized. Not because
I dislike std::variant, but just because I think the option should be
there for those who need/want it.
Regardless, std::variant, in my world, is never empty, since it is
only empty when a move operator throws, which should be never and/or
when I can't allocate 64 bytes, in which case I'm screwed anyhow.
Some days I'm like "man just accept empty, it would be a simple API",
but then I think "it is stupid for variant to go on/off based on whether I
use MS std vs libc++ etc" and also "I don't want double buffering for
cases that only happen in theory, not in practice". I like the
trade-offs of std::variant.
I do agree that Boost should have a variant2 with different choices
than std::variant - I think that actually helps the standard - we can
choose a direction and instead of telling people "tough luck" when
they disagree, we can say "use boost variant2 when you want those
properties".
- Andrzej:
Actually, now I am thinking not about Boost.Outcome but specifying these preconditions in the Standard. If you want to std::terminate upon some condition rather than let the program do random things, you could say.
*Requires:* `!r` with rescue action `std::terminate()`.
This sounds a bit like the contracts proposal - where you get to define what happens when contracts are broken. I'm cautiously hopeful that contracts could help with LEWG's constant struggles with wide vs narrow. - I agree with those who were saying (or thinking) that Boost was - and should again be - the place where APIs go before going into the standard. APIs (mostly) shouldn't bypass boost and go to the standard directly. Or at least an API needs a similar user-base (both in scale and diversity - ie an internal google API has scale, but not necessarily diversity). Since I know a number of LEWG members agree with me on this, don't be surprised to see some push-back from the committee for proposals without that kind of backing, and also don't be surprised if Boost gets flooded with "the committee told me to come here first". Of course I don't speak for LEWG, so maybe none of that happens, we haven't had that discussion yet. I actually probably had more to say - I should have made notes as I was reading all... those... emails. Tony