Discussion:
Feedback requested on D0762R0 draft 1: result<T, E> and expected<T, E>: Summary of the Boost.Outcome
(too old to reply)
Niall Douglas
2017-09-24 01:55:16 UTC
Permalink
Dear std-proposals,

You may remember me asking a few weeks ago about how best to balance a
summary of a peer review for WG21 consumption. Please find attached:

D0762R0 draft 1: result<T, E> and expected<T, E>: Summary of the
Boost.Outcome peer review

And do let me know your thoughts. Does it make any sense? What do you not
find compelling? In particular, do I have insufficient explanatory detail
in places which makes an argument unintelligible?

Thanks in advance,
Niall
--
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposals+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/1b0dc116-044e-4966-8c7b-aeadf0d1b300%40isocpp.org.
Jens Maurer
2017-10-02 20:24:39 UTC
Permalink
D0762R0 draft 1: result<T, E> and expected<T, E>: Summary of the Boost.Outcome peer review
Thanks.

Comments:

I would appreciate a paragraph that introduces the whole topic of "T or E".
Why would I need it at all? My guess is "consider a function that returns
a value T on success and otherwise some error indication E (which might be
an error_code or an exception type)".
(Citing the std::filesystem library with its funny duplicate interface
functions may or may not be helpful.)

Allowing T=void seems useful in some circumstances.

1.1 item 12: You highlight "const T"; what about "volatile T"?
(Fine, I guess, but not highlighted.)

1.1 item 13: "... compatible" should be "convertible"

1.2 item 1: is_base_of_v<std::exception, E> also appears to be an
"error indication" check, and should be considered to be allowed.

I'm curious to learn what those restrictions buy you [reading on...]

1.2 item 2: Constructing to a T or an E seems a bit ... non-obvious,
but I can see why you want that: If you have an error code in hand,
there's little need for extra syntactic sugar when returning.

1.2 item 3: What's "wide/narrow contract" in this case? What does
value() return if there's an E? Checking error() (which might be
"no error") seems plausible before attempting to access the value.

1.2 item 5: What would be a plausible type E where you want to
ignore the result (for the expected case)?

1.2 item 6: I don't understand the reference to optional<result<T>>
here. Where's the E? Would expected's default constructed state
be different from result<T,E>(T()) ?

1.2 item 9: Please show the std:: prefixes throughout, which makes
success and failure, in my opinion, less appealing. And yes, since
"result" only allows a select few types, it seems the failure returns
should be readily apparent in source code.

1.2 item 10: "unexpected_type" seems to be a particularly obtuse
naming choice when reading the expression here.

1.2 item 11 move next to item 3.
It's not clear from the onset whether the a-c list is result's or
expected's behavior. Also, the phrasing "If E is a base of" should be
"... is the same as or derived from...".

Regardless of whether E is constrained (similar to "result"), the behavior
prescribed in item 11 seems reasonable. Oh, and std::error_code has
an "ok" value; can we just return that instead of throwing bad_result_access,
or is that too non-orthogonal?

2.1 "Result is specifically designed..." The argument then talks about
"compile-time overhead for variant storage with strong never-empty
guarantees" For someone not well-versed in the variant wars, could
you please explain (in the paper)? This is all templates in any case,
right? (also mentioned in 3.2)

2.2 second paragraph: "just use expected": We will either have
result or expected in the standard, not both, so "just use expected"
is not an option. And no, make_error_code(enum) should not usually
throw an exception; that would be very surprising (the
standard-provided functions are all noexcept). The third
argument is the compelling one ("the mapping from E to exceptions is
the task of some middle layer; if you don't like the default,
implement your own way of doing it"). This argument should be first.

One of Bjarne's concerns is "make it hard to ignore errors". How is
that reflected in either design, in particular for T=void?

In short, I'm glad to learn that the differences between expected
and result are fairly small, once expected moves away from the
misguided emulation of std::optional (with operator* and friends).

Thanks,
Jens
--
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposals+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/59D2A087.6030907%40gmx.net.
Niall Douglas
2017-10-02 23:16:33 UTC
Permalink
Post by Niall Douglas
Post by Niall Douglas
D0762R0 draft 1: result<T, E> and expected<T, E>: Summary of the
Boost.Outcome peer review
Thanks.
Firstly, thank you very much for taking the time to make such detailed
commentary. I won't reply to the specifics as I received sufficient
feedback on that edition of the paper that I was recommended to throw it
away and try again for a third time at a complete rewrite. You should be
aware that your feedback matched others on WG21, except you were much more
polite about it. So thank you.

I will however comment on the wider points you made, the ones which would
apply to any paper I submit.
Post by Niall Douglas
I would appreciate a paragraph that introduces the whole topic of "T or E".
Why would I need it at all? My guess is "consider a function that returns
a value T on success and otherwise some error indication E (which might be
an error_code or an exception type)".
It's a can of worms that one. I rewrote the tutorial for Outcome v1 three
times, and still the Boost peer review panned it. My lousy attempts were
that bad that Andrezj volunteered to write the v2 Outcome tutorial to keep
me away from it, and you can see his work to date
at https://ned14.github.io/outcome/tutorial/unchecked/

So I'll be honest, anything I write on that will be more confusing than if
I wrote nothing. I'll defer to Vicente's earlier Expected papers for
rationale. The version he's sending to the October mailing, BTW, is
basically the final pre-LWG edition, it is now about implementation
specifics, not rationale for the addition at all.
Post by Niall Douglas
(Citing the std::filesystem library with its funny duplicate interface
functions may or may not be helpful.)
That's another can of worms which gets some people very angry indeed. I
won't bore you with the details here.
Post by Niall Douglas
1.2 item 10: "unexpected_type" seems to be a particularly obtuse
naming choice when reading the expression here.
That's Expected. Be aware that the Toronto meeting renamed it to
`unexpected<E>` now. And it's gained template deduction guides like Outcome
has for `success<T>` and `failure<E>`.
Post by Niall Douglas
Regardless of whether E is constrained (similar to "result"), the behavior
prescribed in item 11 seems reasonable. Oh, and std::error_code has
an "ok" value; can we just return that instead of throwing
bad_result_access,
or is that too non-orthogonal?
Yeah, I'm actually in agreement on that. But the Boost peer review was not.

Also, technically speaking, a default constructed error code is not an ok
value. Nowhere in the standard does it actually guarantee that. Nor can it,
because perhaps on some OS's system category the code 0 is not an ok value,
and is in fact something very bad.

(yes the standard is unhelpfully ambiguous on that, and it's why a
constexpr null category would make so much more sense for a default
initialised error code. Then we can *guarantee* that that is a "no error
here" state)
Post by Niall Douglas
2.1 "Result is specifically designed..." The argument then talks about
"compile-time overhead for variant storage with strong never-empty
guarantees" For someone not well-versed in the variant wars, could
you please explain (in the paper)? This is all templates in any case,
right? (also mentioned in 3.2)
I'll see what I can do about this. But even with explanation, some don't
see the issue. Vicente for example thinks the compiler load for strong
never empty variant storage to not be a problem. I say it does, but my
proof is not the use of an Expected implementation, but rather when I
wrapped std::variant into an Outcome which isn't the same thing because
std::variant is really quite unusually hard on the compiler.

And even then, it all depends on your personal weighting of the importance
of lightweightness of interface files vs header files. And that encompasses
a huge range of opinion, none of which is wrong per se. Though most on SG14
would tend in one direction on that.
Post by Niall Douglas
2.2 second paragraph: "just use expected": We will either have
result or expected in the standard, not both, so "just use expected"
is not an option. And no, make_error_code(enum) should not usually
throw an exception; that would be very surprising (the
standard-provided functions are all noexcept). The third
argument is the compelling one ("the mapping from E to exceptions is
the task of some middle layer; if you don't like the default,
implement your own way of doing it"). This argument should be first.
Cool, thanks.
Post by Niall Douglas
One of Bjarne's concerns is "make it hard to ignore errors". How is
that reflected in either design, in particular for T=void?
Outcome is specifically designed to be used primarily in function returns,
so it is able to make stronger assurances that errors cannot be easily
ignored.

Expected is specifically designed as a primitive on top of which you
construct other stuff. That makes it hard to make such strong assurances.

I personally speaking think that WG21 should standardise *layers* of
implementation for these objects, not some one-size-fits-all single object
like Expected which can never really deliver good practice on its own. But
I'm told the committee don't want that. So the best I can do is submit a
paper saying "please don't do X, Y and Z which the Boost peer review agreed
to be a design mistake relative to other choices", and hope that what gets
standardised doesn't wreck things too badly for future developments.

The docs don't show it yet, but Outcome v2 is precisely such a layered
design (so was v1, but different layers boost-dev didn't care for). You
have:


- unchecked<T, E> - no runtime checking for correct usage at all
- checked<T, E> - throws bad_result_access_with<E> or bad_result_access
if used incorrectly
- result<T, E> - different default behaviours when used incorrectly
depending on user's choice of type E
- outcome<T, E, P> - result but E carries payload P
- outcome<T, EC, E> - result but can be T or EC or E or EC + E. This
neatly sidesteps the std::filesystem loss-of-context API problem.

All these have well defined interoperation semantics and strong ABI
guarantees so big codebases can safely use Outcome in their public API.
Indeed, C code can speak many of these objects, and Outcome provides a C
macro API.

Expected slots somewhere in between unchecked<T, E> and checked<T, E>
because Expected has a wide value observer but narrow observers for
everything else which I find to be a severe design mistake which nobody
should consider after the Optional experience. But clearly a majority on
WG21 do not agree with me.
Post by Niall Douglas
In short, I'm glad to learn that the differences between expected
and result are fairly small, once expected moves away from the
misguided emulation of std::optional (with operator* and friends).
If Expected looked like unchecked<T, E> from Outcome, I'd consider that a
great foundation stone on top of which to build other stuff. Though perhaps
a bit too simple to be used on its own.

But all that said, this stuff can be Conceptualised into generic constructs
and logic, and that ought to be the long term goal for all this stuff with
us taking a least risk approach to threatening that right now. Vicente has
some early ideas on that, I won't share them here, but they are quite
thought provoking.

Niall
--
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposals+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/aebb6cf4-7095-4258-8868-d6eaf287ddd4%40isocpp.org.
Jens Maurer
2017-10-03 07:40:59 UTC
Permalink
Post by Jens Maurer
1.2 item 10: "unexpected_type" seems to be a particularly obtuse
naming choice when reading the expression here.
That's Expected. Be aware that the Toronto meeting renamed it to `unexpected<E>` now. And it's gained template deduction guides like Outcome has for `success<T>` and `failure<E>`.
It would be good if your comparison paper would use the updated terms.
Post by Jens Maurer
Regardless of whether E is constrained (similar to "result"), the behavior
prescribed in item 11 seems reasonable. Oh, and std::error_code has
an "ok" value; can we just return that instead of throwing bad_result_access,
or is that too non-orthogonal?
Yeah, I'm actually in agreement on that. But the Boost peer review was not.
Also, technically speaking, a default constructed error code is not an ok value. Nowhere in the standard does it actually guarantee that. Nor can it, because perhaps on some OS's system category the code 0 is not an ok value, and is in fact something very bad.
I disagree.

Reading 22.5.3 [syserr.errcode], a default-constructed std::error_code
initializes _val = 0, and we later have an "operator bool" that compares
against _val == 0. I believe it's the expected usage pattern that you
invoke "operator bool" to determine whether std::error_code actually
says "here's an error".

I do agree that an xxx_errc enum's value=0 might not yield an "ok"
std::error_code, because make_error_code() for this enum maps away
value == 0 to something else.
Post by Jens Maurer
2.1 "Result is specifically designed..." The argument then talks about
"compile-time overhead for variant storage with strong never-empty
guarantees" For someone not well-versed in the variant wars, could
you please explain (in the paper)? This is all templates in any case,
right? (also mentioned in 3.2)
I'll see what I can do about this. But even with explanation, some don't see the issue. Vicente for example thinks the compiler load for strong never empty variant storage to not be a problem. I say it does, but my proof is not the use of an Expected implementation, but rather when I wrapped std::variant into an Outcome which isn't the same thing because std::variant is really quite unusually hard on the compiler.
And even then, it all depends on your personal weighting of the importance of lightweightness of interface files vs header files. And that encompasses a huge range of opinion, none of which is wrong per se. Though most on SG14 would tend in one direction on that.
There's a lot of stuff in the standard (e.g. stoi) that came in for
"ease of use" motivations, mostly oblivious to the argument "but it's
too slow for my use-case" (which, at least for me, was mostly "shrug,
it's a library facility, if I don't use it, I don't have to care").

result<T,E> (or whatever its final name) should be a widely used low-level
API facility, including being used in a v2 filesystem library etc.
If it's a serious compile-time burden (for example, because it needs to
#include a bunch of stuff), this is not good.

Though adoption of to_chars / from_chars is a move in the right direction,
including a C-compatible interface.
(Full disclosure: I'm the author of to_chars / from_chars.)
Post by Jens Maurer
One of Bjarne's concerns is "make it hard to ignore errors". How is
that reflected in either design, in particular for T=void?
Outcome is specifically designed to be used primarily in function returns, so it is able to make stronger assurances that errors cannot be easily ignored.
... which are (e.g. [[nodiscard]] on the class)? (Please explain in the paper; maybe I've overlooked it.)
Post by Jens Maurer
Expected is specifically designed as a primitive on top of which you construct other stuff. That makes it hard to make such strong assurances.
Uh, that doesn't help the API vocabulary use-case: Having 10 different "on top" things
just fragments the landscape, if developed independently.
Post by Jens Maurer
I personally speaking think that WG21 should standardise /layers/ of implementation for these objects, not some one-size-fits-all single object like Expected which can never really deliver good practice on its own. But I'm told the committee don't want that. So the best I can do is submit a paper saying "please don't do X, Y and Z which the Boost peer review agreed to be a design mistake relative to other choices", and hope that what gets standardised doesn't wreck things too badly for future developments.
We're unlikely to get it right in WG21 if we're not seeing the full set
of such objects.
Post by Jens Maurer
* unchecked<T, E> - no runtime checking for correct usage at all
* checked<T, E> - throws bad_result_access_with<E> or bad_result_access if used incorrectly
* result<T, E> - different default behaviours when used incorrectly depending on user's choice of type E
Maybe here's a fundamental difference in attitude.

From my point of view, we have:
- narrow contract: undefined behavior if used incorrectly
- wide contract: exception showing "bad usage" if used incorrectly

and that's nice from a general viewpoint, but that seems to ignore the "E is an error"
information entirely. I don't see "used incorrectly" here at all for reasonable E.

Let's take .value():
- if there is a T, return it
- if there is no T, there's an E. Since the user called .value(), he (implicitly)
asked for E turned into an exception to be thrown. Make that happen as best as you
can (works for xxx_errc, std::error_code, std::exception_ptr, and std::exception as E
by default; allow ADL customization for the rest, i.e. introduce a "throw_result_exception(E)"
ADL customization point. I'm happy to make that =delete for other E's, requiring the user
to specify a behavior if he wants non-canonical E's.).

Let's take .error():
- if there is an E, return it
- if there is no E, return a default-constructed E, which is supposed to say
"no error".

What's the fall-out?
- We have to say that a default-constructed xxx_errc indicates "no error"
(is there a real-world situation where an OS value 0 actually means "error")?
- We can't reasonably use std::exception as E, because a default-constructed
std::exception() still indicates an error, not "no error".
- std::error_code and std::exception_ptr as E work out-of-the-box.

This seems to support the following usage patterns (hypothetical filesystem library):

std::result<std::path, std::error_code> cwd = std::filesystem::current_path();
(use "auto" if specifying the result type overwhelms you)

#1
std::ifstream input(cwd.value() + "file.txt"); // throw exception if "cwd" didn't work

#2 [is this supported already? if not, has_value() is good enough for me]
if (cwd) { ... } // success

#3 [is this supported already? if not, has_error() is good enough for me]
if (!cwd) return cwd; // propagate error, same T

#4
if (!cwd) return cwd.error(); // propagate error; possibly different T

#5
std::error_code ec = cwd.error(); // process error code
(might be useful if E is some xxx_errc)

(You can, of course, also use has_value() or has_error(), if that better fits
the code pattern in the caller.)

(If E and T are similar enough, e.g. T==E, and you want to deal with this in
generic code that doesn't know about the details of T and E, you probably need
something like success<> and failure<>. But that's probably the right amount
of extra annotations for generic code to see your way through the maze. For
non-generic code, naming something like std::errc::blah already gives enough
hint in the source code that you're talking about an E, so I support implicit
conversion from E to result<T, E>. Same for T.)
Post by Jens Maurer
* outcome<T, E, P> - result but E carries payload P
I'd rather have such a P inside a (more general) "E".
Post by Jens Maurer
* outcome<T, EC, E> - result but can be T or EC or E or EC + E. This neatly sidesteps the std::filesystem loss-of-context API problem.
What's wrong with plugging a "struct { EC, E }" into where "E" goes,
if that's what you want?

Jens
--
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposals+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/59D33F0B.7000607%40gmx.net.
Niall Douglas
2017-10-03 15:59:59 UTC
Permalink
Post by Jens Maurer
Post by Niall Douglas
Also, technically speaking, a default constructed error code is not an
ok value. Nowhere in the standard does it actually guarantee that. Nor can
it, because perhaps on some OS's system category the code 0 is not an ok
value, and is in fact something very bad.
I disagree.
Reading 22.5.3 [syserr.errcode], a default-constructed std::error_code
initializes _val = 0, and we later have an "operator bool" that compares
against _val == 0. I believe it's the expected usage pattern that you
invoke "operator bool" to determine whether std::error_code actually
says "here's an error".
I do agree that an xxx_errc enum's value=0 might not yield an "ok"
std::error_code, because make_error_code() for this enum maps away
value == 0 to something else.
I agree that the standard wording *implies* that a code 0 with system
category is "no error here".

But it cannot claim that without defining what code 0 with system category
is, and it can't do that because it can't know what the system category
coding is for some arbitrary platform.

This is really a defect in the present C++ standard which should be fixed.
As Charley mentioned on sg14, people are thinking about fixing the
non-constexpr constructor of error_code anyway, and that fix will fix this
problem in any case.
Post by Jens Maurer
result<T,E> (or whatever its final name) should be a widely used low-level
API facility, including being used in a v2 filesystem library etc.
If it's a serious compile-time burden (for example, because it needs to
#include a bunch of stuff), this is not good.
I would be surprised if Expected in its present proposal would be
considered usable in stable public ABIs by people with multi million line
codebases.

But that's my experience only from seeing what big multinationals permit in
their C++ interface files. Others will have different experiences, and
different opinions.

Outcome's types are as low impact on the compiler as I can make them. The
only compiler-expensive part is construction because of the fancy
std::variant style constructor design. After that, copying, moving,
everything else is trivially easy for the compiler because it's a plain
struct.

All that said, Outcome v2 has slower compile times than v1 did, and it was
variant based. But it's still not a straight comparison to Expected, I used
a lot of evil tricks which Boost rejected to bring down compiler load. v2
Outcome is straight SFINAE/Concepts based C++, no preprocessor trickery, no
cunning trampoline hacks, no approaching-UB shortcutting.
Post by Jens Maurer
Though adoption of to_chars / from_chars is a move in the right direction,
including a C-compatible interface.
(Full disclosure: I'm the author of to_chars / from_chars.)
A long overdue addition. Thank you for investing the effort.
Post by Jens Maurer
Post by Niall Douglas
Expected is specifically designed as a primitive on top of which you
construct other stuff. That makes it hard to make such strong assurances.
Uh, that doesn't help the API vocabulary use-case: Having 10 different "on top" things
just fragments the landscape, if developed independently.
I agree. Which is why Outcome provides pre-canned primitives which work
well together and with hard ABI guarantees so other stuff can speak them
too.
Post by Jens Maurer
Post by Niall Douglas
I personally speaking think that WG21 should standardise /layers/ of
implementation for these objects, not some one-size-fits-all single object
like Expected which can never really deliver good practice on its own. But
I'm told the committee don't want that. So the best I can do is submit a
paper saying "please don't do X, Y and Z which the Boost peer review agreed
to be a design mistake relative to other choices", and hope that what gets
standardised doesn't wreck things too badly for future developments.
We're unlikely to get it right in WG21 if we're not seeing the full set
of such objects.
I can do no more than now for now. Once the second Boost peer review is
complete, and *if* they approve it for entry into Boost, then I will be
more than happy to come to WG21 at the Rapperswil meeting in 2018 and say
"this set of layered objects is what an extensive peer review produced".
But until then, I can only currently claim "this set of layered objects is
what I personally interpreted from 800+ bits of feedback during the first
peer review, and it may be an incorrect/unwise/wrong interpretation".

But equally I don't want Expected to lose out on entering C++ 20. If you
standardise something approximating Outcome's unchecked<T, E> I think
you're reasonably safe. That's what my current draft of D0762R0 argues for.
Post by Jens Maurer
Post by Niall Douglas
The docs don't show it yet, but Outcome v2 is precisely such a layered
design (so was v1, but different layers boost-dev didn't care for). You
Post by Niall Douglas
* unchecked<T, E> - no runtime checking for correct usage at all
* checked<T, E> - throws bad_result_access_with<E> or
bad_result_access if used incorrectly
Post by Niall Douglas
* result<T, E> - different default behaviours when used incorrectly
depending on user's choice of type E
Maybe here's a fundamental difference in attitude.
- narrow contract: undefined behavior if used incorrectly
- wide contract: exception showing "bad usage" if used incorrectly
and that's nice from a general viewpoint, but that seems to ignore the "E is an error"
information entirely. I don't see "used incorrectly" here at all for reasonable E.
The reason is that E is not necessarily an error for Expected. It's merely
an "unexpected".

For Outcome, we similarly do not require that E is an error. We special
case when it's one of a list of most likely common error types and do treat
E as an error, but otherwise we don't enforce on the user. What we do do is
make life awful for the user by disabling all implicit construction if they
choose E badly. That encourages the user to behave, but doesn't force them.
Post by Jens Maurer
- if there is a T, return it
- if there is no T, there's an E. Since the user called .value(), he (implicitly)
asked for E turned into an exception to be thrown.
Outcome also allows for std::terminate to be called. Or any routine of your
choice.
Post by Jens Maurer
Make that happen as best as you
can (works for xxx_errc, std::error_code, std::exception_ptr, and std::exception as E
by default; allow ADL customization for the rest, i.e. introduce a
"throw_result_exception(E)"
ADL customization point. I'm happy to make that =delete for other E's, requiring the user
to specify a behavior if he wants non-canonical E's.).
Pretty much Outcome v2's design. Slight variation on the throw ADL
customisation point: we provide two points, a policy class mechanism and a
C macro override mechanism.
Post by Jens Maurer
- if there is an E, return it
- if there is no E, return a default-constructed E, which is supposed to say
"no error".
Outcome v1 did this. Boost peer review wanted it gone, so it's gone in v2.

(Eagle eyed folk may notice v2 does it anyway as an implementation detail,
but the docs don't mention it)
Post by Jens Maurer
What's the fall-out?
- We have to say that a default-constructed xxx_errc indicates "no error"
(is there a real-world situation where an OS value 0 actually means "error")?
- We can't reasonably use std::exception as E, because a
default-constructed
std::exception() still indicates an error, not "no error".
- std::error_code and std::exception_ptr as E work out-of-the-box.
std::result<std::path, std::error_code> cwd =
std::filesystem::current_path();
(use "auto" if specifying the result type overwhelms you)
#1
std::ifstream input(cwd.value() + "file.txt"); // throw exception if "cwd" didn't work
Looks good.
Post by Jens Maurer
#2 [is this supported already? if not, has_value() is good enough for me]
if (cwd) { ... } // success
Looks good (and yes boolean test is supported already in both Expected and
Outcome)
Post by Jens Maurer
#3 [is this supported already? if not, has_error() is good enough for me]
if (!cwd) return cwd; // propagate error, same T
Ditto.
Post by Jens Maurer
#4
if (!cwd) return cwd.error(); // propagate error; possibly different T
Works for Outcome as we permit implicit construction for E. For Expected it
would need to be:

if(!cwd) return unexpected(cwd.error());
Post by Jens Maurer
#5
std::error_code ec = cwd.error(); // process error code
(might be useful if E is some xxx_errc)
Also looks good.
Post by Jens Maurer
(You can, of course, also use has_value() or has_error(), if that better fits
the code pattern in the caller.)
(If E and T are similar enough, e.g. T==E, and you want to deal with this in
generic code that doesn't know about the details of T and E, you probably need
something like success<> and failure<>. But that's probably the right amount
of extra annotations for generic code to see your way through the maze. For
non-generic code, naming something like std::errc::blah already gives enough
hint in the source code that you're talking about an E, so I support implicit
conversion from E to result<T, E>. Same for T.)
Ok. You're basically at where the Boost peer review consensus arrived at
eventually.
Post by Jens Maurer
Post by Niall Douglas
* outcome<T, E, P> - result but E carries payload P
I'd rather have such a P inside a (more general) "E".
Ah yes this is non-obvious.

The reason for this is to enhance interoperation between the three-variable
outcome and the two-variable result. So if you, a result returning
function, calls an outcome returning function, you can explicitly cast the
returned outcome into a result which implies either dropping the payload or
having the exception converted or mapped. Outcome provides ADL
customisation points so you can indicate what you want to happen there.

And of course users who want result<T, pair<EC, P>> can absolutely do that
anyway if they want.
Post by Jens Maurer
Post by Niall Douglas
* outcome<T, EC, E> - result but can be T or EC or E or EC + E. This
neatly sidesteps the std::filesystem loss-of-context API problem.
What's wrong with plugging a "struct { EC, E }" into where "E" goes,
if that's what you want?
Some in the Boost peer review felt that it would be useful to be able to
return both an error code and an exception_ptr where the exception_ptr
contains the same error in thrown form, but with additional type erased
payload. Consuming code can then inspect the error code and do something
with that, or rethrow the exception ptr with embedded payload, or a myriad
of other possibilities. And I also mentioned how end users may wish to
configure namespace-local conversion heuristics for localised outcome<T,
EC, E|P> => result<T, EC|E|P>.

To the wider std-proposals, if you're reading this and your head is
beginning to hurt, you're beginning to get some idea of that 800+
contribution Boost peer review. The fact such a small and simple class can
be so vast in scope and effect and consequence is mind boggling.

Niall
--
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposals+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/9998947d-ac3e-4e02-b36c-df1670b30312%40isocpp.org.
Arthur O'Dwyer
2017-10-02 23:04:05 UTC
Permalink
Post by Niall Douglas
Dear std-proposals,
You may remember me asking a few weeks ago about how best to balance a
D0762R0 draft 1: result<T, E> and expected<T, E>: Summary of the
Boost.Outcome peer review
Thanks for this paper!

I find it confusing that the paper uses the words "Outcome" and "Result"
interchangeably or at least indistinguishably to the layman. I infer that
the thing called Boost::Outcome is being proposed for standardization as
Std::Result? I assume there are good pedantic reasons for saying "Outcome"
sometimes and "Result" other times, but it's needlessly confusing to me
IMHO.
Post by Niall Douglas
(a) std::is_base_of_v<std::error_code, E> must be true; OR
(b) std::is_base_of_v<std::exception_ptr, E> must be true; OR [...]
If the user is actually inheriting from either std::error_code or
std::exception_ptr, they are in a state of sin. Those types are *not*
supposed to be classically polymorphic, and inheriting from them will only
lead to slicing and bitter tears. Would anything of value be lost if you
replaced "is_base_of" with "is_same", here?

Dissimilarity #2 (implicit constructors) seems like a clear win for
Expected. Implicit construction (especially from integral types) is the
root of all evil.

Dissimilarity #5 says "nor would it be appropriate for many choices of type
E." This needs explanation/support. Otherwise it seems like just a QoI
defect against Expected.

Dissimilarity #9 says "Expected’s type sugar is unexpected_type<E> which
maps almost identically onto failure<E>. Expected has no corollary to
success<T = void> as it does not permit implicit construction from E." I
think the last sentence's first clause is confused. I would write these two
sentences as: "Expected's syntactic sugar for failure(E) is
make_unexpected(E). Expected's syntactic sugar for success(T) is
make_expected(T). Both Expected and Result are implicitly constructible
from T."
Likewise in #10 I would think that
"std::make_unexpected<std::error_code>(GetLastError(),
std::system_category())" would be a fairer comparison.

Re narrow and wide observers: The general rule in my experience is that
things-using-punctuation are narrow-contract and things-using-English-names
are wide-contract. Thus wide optional::value(), wide vector::at(), narrow
optional::operator*(), narrow vector::operator[](). If Expected is
breaking this rule, then yeah, that sucks.

Section 3.2 "De-variant Expected?" mildly confuses me. You keep saying that
Expected<T,E> uses "variant storage" and that this is a bad thing for its
ABI. But then dissimilarity #7 shows the implementation of Result<T,E> and
I see a union there too! So, if you're using the term "variant storage" to
mean something different from "a union", then what do you mean by it? I've
implemented std::variant and Expected and I can tell you, (A), std::variant
is just a big union, and (B), nobody should be implementing Expected (or
std::optional) *literally* in terms of a header dependency on <variant>.
Both std::variant and Expected involve unions at some level, sure; but it
seems from the paper that Result<T,E> *also* involves a union.

Line 610 of the code in the appendix has a "Pxxxx" that maybe should be
filled in at this point?

HTH,
Arthur
--
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposals+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/f3a9a0ec-2060-4513-90ee-864e742e49c8%40isocpp.org.
Niall Douglas
2017-10-02 23:42:45 UTC
Permalink
Also thank you for the detailed response. As just explained, that paper is
now dustbinned due to other WG21 feedback, but in response to the general
Post by Arthur O'Dwyer
If the user is actually inheriting from either std::error_code or
std::exception_ptr, they are in a state of sin. Those types are *not*
supposed to be classically polymorphic, and inheriting from them will only
lead to slicing and bitter tears. Would anything of value be lost if you
replaced "is_base_of" with "is_same", here?
Alas subclassing error code with additional payload happens frequently
enough precisely because error code can't carry payload, and we often need
it to. Indeed Outcome v1 supplied an error_code_extended which subclassed
error_code. You may be surprised to learn that the Boost peer review liked
that quite a lot indeed, and I will receive flak in the second peer review
for dropping it in v2.
Post by Arthur O'Dwyer
Dissimilarity #2 (implicit constructors) seems like a clear win for
Expected. Implicit construction (especially from integral types) is the
root of all evil.
The Boost peer review went into this in unbelievable depth. And we came out
on the other side with a set of constraints which the consensus believed
would prevent the problems which say std::variant has.

So Outcome replicates std::variant's constructor design, but it is far more
constrained than std::variant.

To test these constraints, I replaced v1 Outcome with v2 in an existing
large code base, plus wrote lots of new code using v2 Outcome to give it a
thorough kicking. Precisely once did unexpected surprise occur with the
constraints given by the Boost peer review, and I have poked a single hole
for that one use case which may or may not be felt to be acceptable in the
second peer review.

I'm going to claim that implicit construction for this specific use case
with those constraints is safe. And therefore Expected is being needlessly,
and wastefully, over cautious.
Post by Arthur O'Dwyer
Dissimilarity #5 says "nor would it be appropriate for many choices of
type E." This needs explanation/support. Otherwise it seems like just a QoI
defect against Expected.
Yeah Vicente raised that one too. The problem is we cannot inspect a type
to see if it's marked [[nodiscard]].
Post by Arthur O'Dwyer
Re narrow and wide observers: The general rule in my experience is that
things-using-punctuation are narrow-contract and things-using-English-names
are wide-contract. Thus wide optional::value(), wide vector::at(), narrow
optional::operator*(), narrow vector::operator[](). If Expected is
breaking this rule, then yeah, that sucks.
That rule needs to go die somewhere. In the Boost peer review, we
eventually arrived at a new rule which was that all-narrow or all-wide with
clearly distinct naming is the right path forwards. Mixed wide-narrow leads
to broken badly written code which is non-obviously broken and badly
written.
Post by Arthur O'Dwyer
Section 3.2 "De-variant Expected?" mildly confuses me. You keep saying
that Expected<T,E> uses "variant storage" and that this is a bad thing for
its ABI. But then dissimilarity #7 shows the implementation of Result<T,E>
and I see a union there too! So, if you're using the term "variant
storage" to mean something different from "a union", then what do you mean
by it? I've implemented std::variant and Expected and I can tell you, (A),
std::variant is just a big union,
A conforming std::variant does a ton load more than be just a big union.
And a strong never empty guaranteed variant storage like Expected provides
means a ton load more again implementation code, much of which is complex
for the compiler to instantiate i.e. lots of SFINAE and concept matching.
You've got potential double buffering in there, you've got to preserve
triviality as well, plus alignment requirements must be met.

I too have implemented this for v1 Outcome. It is highly non-trivial,
either for the programmer or the compiler.
Post by Arthur O'Dwyer
and (B), nobody should be implementing Expected (or std::optional)
*literally* in terms of a header dependency on <variant>.
Vicente said the same thing.

I replied: "why not?"

I would also mention [1]
https://isocpp.org/blog/2017/07/what-should-the-iso-cpp-standards-committee-be-doing
which would strongly suggest this:

1. If you are adding a feature which uses 98% of an existing feature, then
the standard should *require *use of that existing feature.

2. If you don't require it, then you are either saying that the existing
feature has something wrong with it, or your new feature has something
wrong with it. Or you hate the C++ ecosystem. It's one of these three.

3. By requiring it you force existing features to be broken up into well
defined implementation layers. This is a very, very good thing which must
be encouraged to achieve [1].

But I appreciate that almost nobody on WG21 shares this viewpoint, and
historically the standard doesn't dictate implementation. I'm saying it's
time it began to (in terms of its own standard library).

Niall
--
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposals+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/ba560292-2b30-44ff-8a37-96c95c2abd24%40isocpp.org.
Nicol Bolas
2017-10-03 02:43:22 UTC
Permalink
Post by Niall Douglas
Also thank you for the detailed response. As just explained, that paper is
now dustbinned due to other WG21 feedback, but in response to the general
Post by Arthur O'Dwyer
If the user is actually inheriting from either std::error_code or
std::exception_ptr, they are in a state of sin. Those types are *not*
supposed to be classically polymorphic, and inheriting from them will only
lead to slicing and bitter tears. Would anything of value be lost if you
replaced "is_base_of" with "is_same", here?
Alas subclassing error code with additional payload happens frequently
enough precisely because error code can't carry payload, and we often need
it to.
That doesn't justify *deriving* from it. A member would be just as
effective, as would providing implicit conversion to `error_code` if that's
the sort of interface someone wants to use.

And deriving from *exception_ptr* is even more wrong.

Yet another reason to prefer `expected`: it doesn't encourage you to use
poor interfaces.

Indeed Outcome v1 supplied an error_code_extended which subclassed
Post by Niall Douglas
error_code. You may be surprised to learn that the Boost peer review liked
that quite a lot indeed, and I will receive flak in the second peer review
for dropping it in v2.
Post by Arthur O'Dwyer
Re narrow and wide observers: The general rule in my experience is that
things-using-punctuation are narrow-contract and things-using-English-names
are wide-contract. Thus wide optional::value(), wide vector::at(), narrow
optional::operator*(), narrow vector::operator[](). If Expected is
breaking this rule, then yeah, that sucks.
That rule needs to go die somewhere.
... why? I hadn't even realized that this rule exists, and yet as I look at
it, it makes perfect sense. The short, simple, C-like form is the form that
doesn't throw. The longer, more verbose form is the form that throws. It's
a simple and obvious rule, which C++ programmers already have to know,
since the many C constructs that already exist abide by it. If you take a
naked pointer and use `*` on it, you don't get an exception if its a NULL
pointer. If you use `[]` on it, you don't get bounds checking. C++ objects
that mimic such types should work the same way.

Also, operators are more likely to be found in complex expressions than
simple function calls. And throwing exceptions from within complex
expressions can be dangerous, due to indeterminate order of evaluation. So
even there it makes sense.

This is a good rule, and `expected`'s interface ought to abide by it (where
reasonable).

In the Boost peer review, we eventually arrived at a new rule which was
Post by Niall Douglas
that all-narrow or all-wide with clearly distinct naming is the right path
forwards. Mixed wide-narrow leads to broken badly written code which is
non-obviously broken and badly written.
It also leads to code where you only check for errors *once*, rather than
every time you access something.

Section 3.2 "De-variant Expected?" mildly confuses me. You keep saying that
Post by Niall Douglas
Post by Arthur O'Dwyer
Expected<T,E> uses "variant storage" and that this is a bad thing for its
ABI. But then dissimilarity #7 shows the implementation of Result<T,E> and
I see a union there too! So, if you're using the term "variant storage" to
mean something different from "a union", then what do you mean by it? I've
implemented std::variant and Expected and I can tell you, (A), std::variant
is just a big union,
A conforming std::variant does a ton load more than be just a big union.
He was talking about at the ABI level. His point is that the ABI for a
Result<T, E> and the ABI for Expected<T, E> is the same: they both store a
union and they both store a value which tells which union member is active.
And a `variant<T, E>` also stores a union and a value that tells which
union member is active.

`Result` uses "variant storage" just as much as `Expected`.

And a strong never empty guaranteed variant storage like Expected provides
Post by Niall Douglas
means a ton load more again implementation code, much of which is complex
for the compiler to instantiate i.e. lots of SFINAE and concept matching.
You've got potential double buffering in there,
P0110 shows that you don't have to double-buffer to get the strong never
empty guarantee. The proposal already requires, if you do
assignment/emplacement, that `E` is nothrow move constructible. That allows
you to avoid double buffering.

That's not to say that the code isn't complex. But there is no potential
for double buffering.

you've got to preserve triviality as well, plus alignment requirements must
Post by Niall Douglas
be met.
Are you saying that Result<T, E> doesn't preserve triviality and alignment?

I too have implemented this for v1 Outcome. It is highly non-trivial,
Post by Niall Douglas
either for the programmer or the compiler.
and (B), nobody should be implementing Expected (or std::optional)
Post by Arthur O'Dwyer
*literally* in terms of a header dependency on <variant>.
Vicente said the same thing.
I replied: "why not?"
Because that's not how specifications work. That's really the only answer I
can give you.

I would also mention [1]
Post by Niall Douglas
https://isocpp.org/blog/2017/07/what-should-the-iso-cpp-standards-committee-be-doing
I see nothing in that paper which "strongly suggests" anything of the sort.

1. If you are adding a feature which uses 98% of an existing feature, then
Post by Niall Douglas
the standard should *require *use of that existing feature.
2. If you don't require it, then you are either saying that the existing
feature has something wrong with it, or your new feature has something
wrong with it. Or you hate the C++ ecosystem. It's one of these three.
Or it's because that's not how specifications work. Because *that's not how
specifications work*.

Specifications define visible behavior. The programmer writes something
against a specification. Implementations provide a translation layer
between the user's writing and the actual machine, in accord with the
behavior defined by the specification. And thus, the user can see the
visible behavior, based on their writing against the specification.

Whether `optional` is implemented in terms of `variant` is *not* something
which constitutes "visible behavior". As such, it is not the right of the
standard to require such a thing.

3. By requiring it you force existing features to be broken up into well
Post by Niall Douglas
defined implementation layers. This is a very, very good thing which must
be encouraged to achieve [1].
Nonsense. None of the ideals outlined in that paper explicitly or
implicitly require having "well defined implementation layers". The
stability goals can be achieved by the implementation, not by standard fiat.

But I appreciate that almost nobody on WG21 shares this viewpoint, and
Post by Niall Douglas
historically the standard doesn't dictate implementation. I'm saying it's
time it began to (in terms of its own standard library).
The moment the standard starts requiring a specific implementation is the
moment it stops being a standard and starts being an implementation of some
standard. The committee doesn't "historically" dictate implementations
because *it's not their job*.
Post by Niall Douglas
Niall
--
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposals+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/f90d8cd7-d12c-44c4-8f4f-78ebe10ab344%40isocpp.org.
Niall Douglas
2017-10-03 15:21:33 UTC
Permalink
Post by Nicol Bolas
Yet another reason to prefer `expected`: it doesn't encourage you to use
poor interfaces.
A lot of these sweeping hand waving generalisations of yours unfortunately
seem to repeatedly come from ignorance. It's hardly the first time you've
made these unfounded claims, and you did so repeatedly throughout your
reply.

Outcome's checked<T, E> is very similar to expected<T, E>. Do your research
before making more claims without basis.
Post by Nicol Bolas
`Result` uses "variant storage" just as much as `Expected`.
Outcome uses struct storage, not union storage.
Post by Nicol Bolas
And a strong never empty guaranteed variant storage like Expected provides
Post by Niall Douglas
means a ton load more again implementation code, much of which is complex
for the compiler to instantiate i.e. lots of SFINAE and concept matching.
You've got potential double buffering in there,
P0110 shows that you don't have to double-buffer to get the strong never
empty guarantee. The proposal already requires, if you do
assignment/emplacement, that `E` is nothrow move constructible. That allows
you to avoid double buffering.
That's not to say that the code isn't complex. But there is no potential
for double buffering.
D0323R3 currently entirely deletes assignment unless E is nothrow move
constructible, thus allowing avoidance of having to consider double
buffering.
Post by Nicol Bolas
The moment the standard starts requiring a specific implementation is the
moment it stops being a standard and starts being an implementation of some
standard. The committee doesn't "historically" dictate implementations
because *it's not their job*.
Things change as standard libraries grow.
Niall
--
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposals+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/1f550260-377a-4409-b29e-dee0769d6bd9%40isocpp.org.
Jens Maurer
2017-10-03 17:25:31 UTC
Permalink
The moment the standard starts requiring a specific implementation is the moment it stops being a standard and starts being an implementation of some standard. The committee doesn't "historically" dictate implementations because /it's not their job/.
Well, the standard certainly shouldn't prescribe a specific
implementation, but it could prescribe certain properties
that are useful, for example when interacting with C.

See, for example, std::pair: It has a trivial destructor if
both involved types have trivial destructors.

We could similarly prescribe the triviality of copy and move
constructors.

Now, it's obviously hard to argue about C compatibility for
something that is a template and has a plethora of interesting
constructors. However, we already attempt something like
that for std::complex; see 29.5 p4 [complex.numbers].

Maybe we want to say something like std::result and a struct X
consisting of T, unsigned int, and E members have a common
initial sequence that is all of X.

Jens
--
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposals+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/59D3C80B.4050402%40gmx.net.
Arthur O'Dwyer
2017-10-03 18:27:21 UTC
Permalink
First, I should also say thank you to Jens Maurer for writing that initial
reply to Niall's paper, since I wouldn't have looked at the paper if Jens
hadn't done so first. Even if it turned out that that paper is already
obsolete in Niall's view.

This thread is getting super tangled, so I won't try to respond directly to
any quotes in context; I'll just list some of my opinions in the order I
come to them. If something here looks like I'm agreeing-with or
contradicting something you personally said in this thread, then yeah, I'm
probably responding to you. ;)


(1) A default-constructed std::error_code is absolutely an "ok, no error"
value. In fact, for any enumeration type E where is_error_code_enum_v<E>,
the value static_cast<E>(0) must be the "ok, no error" value. I think we
are all in agreement that the Standard strongly implies that stuff breaks
if you deviate from this informal rule. Niall referred to the absence of
normative wording as "really a defect." Yes it is. I would like us all to
assume that the defect will be fixed, and waste no further verbiage on
pedantry in that area.

(2) Re a tricky idea Jens suggested: `make_error_code(E)` should not do any
value-munging "tricks" (such as remapping E(200) to int(0), or E(0) to
int(200)), because such tricks would should be undone by
`static_cast<E>(ec.code())`, and those semantics are 100% nailed down by
the Standard.

(3) It seems very important to Niall that the Expected/Result type that is
standardized should have a normatively defined class definition, so that
anyone could look at the Standard wording and deduce exactly how to
interoperate with Expected/Result from C code or from pre-C++2a code. I
sympathize with this idea. I think Nicol was right when he said that the
C++ Standard generally does *not* standardize class definitions. So just be
aware that you will be swimming upstream to get this — especially if you
want a non-standard-layout class with special member functions, private
members, etc etc. Jens gives the example of std::complex.

(4) I believe that the world would be a better place if the C++ Standard
*did* sometimes standardize class definitions. And the world would be
better if they *did* sometimes standardize a function's behavior via
"reference implementation."

(5) I still believe that it is a sin to subclass std::error_code,
std::exception_ptr, or any other std:: class type that is not already part
of a classical hierarchy. Composition/aggregation is okay. Inheritance is
not. You will get slicing and you will get bugs.

(6) Jens's example use-case #4 almost convinced me that
implicit-construction-from-the-E-type is desirable. `if (!cwd) return
cwd.error()` is easier on the eyes than `if (!cwd) return
make_unexpected(cwd.error())`. However, I wonder whether we could get the
best of both worlds by providing a convenience member function: `if (!cwd)
return cwd.unexpected()`. For the long-term problems caused by implicit
conversions involving vocabulary types, please see the ongoing fiasco of
std::string, std::string_view, std::filesystem::path. Eliminating implicit
conversions from your C++ code eliminates bugs. Eliminating implicit
conversions from your proposals eliminates literally YEARS of work for the
future members of the Committee.

(7) I don't like the term "never-empty variant." Even Anthony Williams'
P0110 (which explores the idea of making a variant twice as big as
std::variant so that the new object could always be constructed before the
old one was destroyed) finally admits that he made emplace<T>() destroy the
old object before constructing the new one, and that means that his variant
*also* has an empty state. A variant which *requires* an empty state to
exist is not "never-empty."

(8) I certainly hope that both Result<T,E> and Expected<T,E> are "X"
whenever both T and E are "X", for values of "X" including at least
"trivially copyable" and "trivially destructible". If some implementation
of Result or Expected does not provide those guarantees, then that's a QoI
issue against that implementation. (I hope that the version that gets
standardized will have normative wording *requiring* those guarantees. If
that happens, then implementations without those guarantees will be
non-conforming.)

(9) Niall wrote: "If you are adding a feature which uses 98% of an existing
feature, then the standard should require use of that existing feature." I
strongly disagree with that statement. Now, I *do* think (and I think Niall
might agree) that we should not add new features with small gratuitous
quirks that make them impossible to implement in terms of other standard
features. To take two examples of the "gratuitous quirk" problem:
std::packaged_task<R(A...)> is not implementable in terms of
std::function<R(A...)> because copyability, and std::vector<T,A>::resize is
not implementable in terms of std::uninitialized_move_if_noexcept() because
allocator_traits. Therefore, if it were truly *impossible* to implement
Expected<T,E> in terms of std::variant<T,E>, I'd complain.
HOWEVER, I repeat that no sane implementation should pull in <variant> as a
result of including <expected>. The former is a "higher-level" feature than
the latter; it includes a ton of TMP machinery that is not needed by
<expected>. I might expect both headers to include some "helper" header
providing just the common parts needed by both.
We see an example of this in both libstdc++ and libc++, where <set> and
<map> are implemented in terms of a "helper" header sometimes named
<__tree>. It is theoretically possible to implement std::map<K,V> in terms
of std::set<something_clever<K,V>>, but no sane implementation will pull in
all of <set> as a result of including <map>.
I hope this clarifies my position.

(10) Speaking of "levelization", I do like the idea of having
"higher-level" and "lower-level" headers in the STL. I think it's
unfortunate that <system_error> — which sounds like a low-level header —
actually depends on <string>, which recursively depends on most of the STL
(<string_view>, <ostream>, et cetera). I talked to Charley about this at
CppCon: I'm ambivalent. On the one hand I think it really sucks that
<system_error> has any dependencies at all. On the other hand, I appreciate
the idea of "vocabulary types," and it feels hypocritical to condemn the
library's own use of `std::string` as the vocabulary type for returning a
string.

(11) Niall writes: "Outcome uses struct storage, not union storage."
The paper Jens and I were reading flatly contradicts that statement, on
page 4, where it says "Result requires the following layout to be
implemented:" and then shows a piece of code involving a union. However,
the reference C implementation
on page 14 uses a non-union struct, and the reference C++ implementation on
page 17 uses a "non-standard" optional<T> with the caveats listed in a
comment at the bottom of page 16. It would be much *much* clearer if the
new paper didn't imply the use of a union/optional/variant; it would save
Niall a lot of argumentation. If you want a standard-layout struct, just
use one! Don't even put "union" in your sample code!

(12) Niall writes: "E is not necessarily an error for Expected. It's merely
an 'unexpected'." This is a distinction without a difference. Whether we
call it an "error", an "unexpected result", or an "exceptional state", the
point is that it's a "disappointment" (to use Lawrence Crowl's term). The
user's expectations are that foo.value() will give them the normal-path
result (if any), and that foo.error() will give them the
"disappointment-path" result (if any). Jens points out, rightly, that if
the user asks for foo.error() and there is no disappointment to report, it
makes some sense to return "no error" as opposed to blowing up the program
via a thrown exception or via undefined behavior. As to the question of
whether we can generally consider a default-constructed E to indicate "no
error", please see my point (1) in this email.

(13) I don't see the usefulness of outcome<X,Y,Z>; but I know that if I
don't like it I can avoid using it, and it sounds like Niall is not
proposing that one for standardization anyway. (That is, he's proposing it
for Boost.Outcome but not for C++2a.)

–Arthur
--
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposals+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/CADvuK0%2B4x1LUi%3DzAxDz2TY8af%3DGPxis5rnmcLLjYFTQe7fbQJg%40mail.gmail.com.
Ville Voutilainen
2017-10-03 18:40:51 UTC
Permalink
Post by Arthur O'Dwyer
(5) I still believe that it is a sin to subclass std::error_code,
std::exception_ptr, or any other std:: class type that is not already part
of a classical hierarchy. Composition/aggregation is okay. Inheritance is
not. You will get slicing and you will get bugs.
Fascinating. I didn't realize the writings that people usually use to
define what is a sin
talk about C++. I must update my understanding. :) With that
digression aside, presumably
you have a problem with public inheritance, not necessarily with
private inheritance?
--
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposals+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/CAFk2RUbVhTEbASYA%2BnAL8h2th6BVbKOP%2BzQOsCnR3M29AZ%3DNtg%40mail.gmail.com.
Arthur O'Dwyer
2017-10-03 18:58:31 UTC
Permalink
On Tue, Oct 3, 2017 at 11:40 AM, Ville Voutilainen <
Post by Ville Voutilainen
Post by Arthur O'Dwyer
(5) I still believe that it is a sin to subclass std::error_code,
std::exception_ptr, or any other std:: class type that is not already
part
Post by Arthur O'Dwyer
of a classical hierarchy. Composition/aggregation is okay. Inheritance is
not. You will get slicing and you will get bugs.
Fascinating. I didn't realize the writings that people usually use to
define what is a sin
talk about C++. I must update my understanding. :) With that
digression aside, presumably
you have a problem with public inheritance, not necessarily with
private inheritance?
Well, personally I would also avoid private inheritance; but yes, public
inheritance is the much bigger problem for things like slicing.

In Niall's case, he's checking std::is_base_of, which will report true even
for private inheritance <https://stackoverflow.com/a/2911185/1424877>, even
though *I assume* the things he's going to try to do with the "E" type will
eventually require public inheritance (e.g. convertibility).

I wish there were a standard type-trait for checking the "public
unambiguous base" relationship, because I strongly believe that that's the
relationship most programmers really care about. Being an inaccessible (
*usually* synonymous with non-public) or ambiguous base class is not a
relationship that I *usually* care about.
https://wandbox.org/permlink/jzjqQAHhpvPnReCM

–Arthur
--
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposals+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/CADvuK0JO-B0PiRhCU27sL_kxE3f%3D-uaOJ5PbstjSpB%2BMiuUZ2A%40mail.gmail.com.
Ville Voutilainen
2017-10-03 19:07:18 UTC
Permalink
I wish there were a standard type-trait for checking the "public unambiguous
base" relationship, because I strongly believe that that's the relationship
most programmers really care about. Being an inaccessible (usually
synonymous with non-public) or ambiguous base class is not a relationship
that I usually care about.
https://wandbox.org/permlink/jzjqQAHhpvPnReCM
An alternative take is
template<class T, class U>
struct is_public_unambiguous_base_of :
std::conjunction<std::is_class<T>, std::is_class<U>,
std::is_convertible<U*, T*>> {};
--
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposals+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/CAFk2RUZuz6vg8o3vi%2BknBtfOf65Qwkfk%3DQCs%2Bw8hYqVQBySJdw%40mail.gmail.com.
Niall Douglas
2017-10-03 21:36:46 UTC
Permalink
Post by Arthur O'Dwyer
In Niall's case, he's checking std::is_base_of, which will report true
even for private inheritance <https://stackoverflow.com/a/2911185/1424877>,
even though *I assume* the things he's going to try to do with the "E"
type will eventually require public inheritance (e.g. convertibility).
I only use std::is_base_of because it's fast for the compiler to evaluate,
not because it's correct. If we had Concepts, you'd actually ask "does this
type quack like a std::error_code?"

I would recommend std-proposals consider my use of it as an irrelevant
implementation detail confined to a potential Boost.Outcome library only.
You'll notice interim draft 2 paper has completely dropped any mention of
std::is_base_of as it is distracting from the main message.

Niall
--
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposals+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/13b3bfac-66f0-4c1a-a32e-8745450b7ed4%40isocpp.org.
Jens Maurer
2017-10-03 19:30:24 UTC
Permalink
Post by Arthur O'Dwyer
(1) A default-constructed std::error_code is absolutely an "ok, no
error" value. In fact, for any enumeration type E where
is_error_code_enum_v<E>, the value static_cast<E>(0) must be the "ok,
no error" value.
Wait, std::error_code and the enumeration E are different things.
std::error_code stores an "int", produced from E via make_error_code(),
which could (conceivably) map E's values to something else.
Post by Arthur O'Dwyer
I think we are all in agreement that the Standard
strongly implies that stuff breaks if you deviate from this informal
rule. Niall referred to the absence of normative wording as "really a
defect." Yes it is. I would like us all to assume that the defect
will be fixed, and waste no further verbiage on pedantry in that
area.
Fine with me.
Post by Arthur O'Dwyer
(2) Re a tricky idea Jens suggested: `make_error_code(E)` should not
do any value-munging "tricks" (such as remapping E(200) to int(0), or
E(0) to int(200)), because such tricks would should be undone by
`static_cast<E>(ec.code())`, and those semantics are 100% nailed down
by the Standard.
It's "ec.value()" apparently; and where does it say something about
the static_cast being meaningful (a section / paragraph reference
would help)?
Post by Arthur O'Dwyer
(5) I still believe that it is a sin to subclass std::error_code,
std::exception_ptr, or any other std:: class type that is not already
part of a classical hierarchy. Composition/aggregation is okay.
Inheritance is not. You will get slicing and you will get bugs.
Agreed.
Post by Arthur O'Dwyer
(6) Jens's example use-case #4 almost convinced me that
implicit-construction-from-the-E-type is desirable. `if (!cwd) return
cwd.error()` is easier on the eyes than `if (!cwd) return
make_unexpected(cwd.error())`. However, I wonder whether we could get
`if (!cwd) return cwd.unexpected()`.
I should probably say here that I find the name "unexpected"
(or "expected", for that matter) particularly unhelpful.
In my world, there's nothing unexpected about an error,
and the discussion about similarities vs. std::variant
seems to indicate that T and E are sometimes viewed as being
on the same level (they're not; E is still an error).
Post by Arthur O'Dwyer
For the long-term problems
caused by implicit conversions involving vocabulary types, please see
the ongoing fiasco of std::string, std::string_view,
std::filesystem::path. Eliminating implicit conversions from your C++
code eliminates bugs. Eliminating implicit conversions from your
proposals eliminates literally YEARS of work for the future members
of the Committee.
It seems to me that some of that wrapping might cause extra copies
or moves to stick the "E" instance into the wrapper before passing
it to the std::result constructor. Also, I'd be happy to allow
only exact matches for E, i.e. you can only implicitly convert an E
to a std::result<T,E>, not to a std::result<T, E2>, where E converts
to E2. That seems a lot safer than the situation with std::string and
friends.
Post by Arthur O'Dwyer
(10) Speaking of "levelization", I do like the idea of having
"higher-level" and "lower-level" headers in the STL. I think it's
unfortunate that <system_error> — which sounds like a low-level
header — actually depends on <string>, which recursively depends on
most of the STL (<string_view>, <ostream>, et cetera). I talked to
Charley about this at CppCon: I'm ambivalent. On the one hand I think
it really sucks that <system_error> has any dependencies at all. On
the other hand, I appreciate the idea of "vocabulary types," and it
feels hypocritical to condemn the library's own use of `std::string`
as the vocabulary type for returning a string.
It seems, since std::result<T,E> is a template, we can stick it into
its own header and only people wanting E = std::error_code need to
pull in <system_error> (and its dependencies) themselves. People
wanting E = their_own_enum will be fine as-is.
Post by Arthur O'Dwyer
(11) Niall writes: "Outcome uses struct storage, not union storage."
The paper Jens and I were reading flatly contradicts that statement,
on page 4, where it says "Result requires the following layout to be
implemented:" and then shows a piece of code involving a union.
However, the reference C implementation on page 14 uses a non-union
struct, and the reference C++ implementation on page 17 uses a
"non-standard" optional<T> with the caveats listed in a comment at
the bottom of page 16. It would be much *much* clearer if the new
paper didn't imply the use of a union/optional/variant; it would save
Niall a lot of argumentation. If you want a standard-layout struct,
just use one! Don't even put "union" in your sample code!
There's a union where an "unsigned int" is shared with E; this should
simply go. If you want to show standard-layout, just show

struct result {
T _value;
/* integral or so */ discriminator;
E _error;
};

However, prescribing this order of members might be sub-optimal in
terms of alignment / packing of T and E; some other order might
yield a smaller "result" layout on some platforms for some T and E.
Post by Arthur O'Dwyer
(12) Niall writes: "E is not necessarily an error for Expected. It's
merely an 'unexpected'." This is a distinction without a difference.
Whether we call it an "error", an "unexpected result", or an
"exceptional state", the point is that it's a "disappointment" (to
use Lawrence Crowl's term). The user's expectations are that
foo.value() will give them the normal-path result (if any), and that
foo.error() will give them the "disappointment-path" result (if any).
Jens points out, rightly, that if the user asks for foo.error() and
there is no disappointment to report, it makes some sense to return
"no error" as opposed to blowing up the program via a thrown
exception or via undefined behavior. As to the question of whether we
can generally consider a default-constructed E to indicate "no
error", please see my point (1) in this email.
I'm not convinced we should restrict E to the set of types Niall
has in his paper; if we have a more general E, it's maybe a harder
sell that a default-constructed E should never actually indicate an
error.
Post by Arthur O'Dwyer
(13) I don't see the usefulness of outcome<X,Y,Z>; but I know that if
I don't like it I can avoid using it, and it sounds like Niall is not
proposing that one for standardization anyway. (That is, he's
proposing it for Boost.Outcome but not for C++2a.)
There's certainly the issue that just having a std::error_code as E
is not good enough if you also want to propagate some context to
upper layers (e.g. which function failed to create a file using which
pathname). I sympathize, but I still feel allowing something custom
such as

struct my_E {
std::error_code ec;
std::filesystem::path failed_path;
std::string failed_function;
};

is the way to go here. If, on your way out, you need to strip
that info down (maybe after logging) to E = std::error_code, you
just pick the relevant member. (I have a feeling that creating
a std::exception_ptr is non-cheap, so requiring people to transport
extra info via that mechanism seems not a good idea.)

As a general remark, error handling seems very much a system-level
design exercise (regardless of whether using exceptions or std::result),
so I fully expect each code base to have an opinion on what E's they
permit or desire.

Further, I'm not of the opinion that the standard should provide
everything to everybody, so slightly burdening someone wanting a my_E
(e.g. with an additional ADL customization point to cater to) is
fine with me.

Jens
--
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposals+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/59D3E550.9050109%40gmx.net.
Arthur O'Dwyer
2017-10-03 20:07:06 UTC
Permalink
Post by Jens Maurer
Post by Arthur O'Dwyer
(1) A default-constructed std::error_code is absolutely an "ok, no
error" value. In fact, for any enumeration type E where
is_error_code_enum_v<E>, the value static_cast<E>(0) must be the "ok,
no error" value.
Wait, std::error_code and the enumeration E are different things.
std::error_code stores an "int", produced from E via make_error_code(),
which could (conceivably) map E's values to something else.
This was my (2). ;)
Post by Jens Maurer
(2) Re a tricky idea Jens suggested: `make_error_code(E)` should not
Post by Arthur O'Dwyer
do any value-munging "tricks" (such as remapping E(200) to int(0), or
E(0) to int(200)), because such tricks would should be undone by
`static_cast<E>(ec.code())`, and those semantics are 100% nailed down
by the Standard.
It's "ec.value()" apparently; and where does it say something about
the static_cast being meaningful (a section / paragraph reference
would help)?
Good catch, I meant .value() not .code(). (".code()" turns a system_error
into an error_code. ".value()" turns an error_code into an int.)
This is the pedantry I don't want to get into, so I won't. (But examine the
specifications for error_code::message() and error_category::message() and
see if you think there's any room for the int values to be different from
the enumeration values. And then remember that we've already agreed this is
a defect and we want to close any loopholes in it, so maybe don't think too
hard about ways to exploit those loopholes in the meantime?)
Post by Jens Maurer
(6) Jens's example use-case #4 almost convinced me that
Post by Arthur O'Dwyer
implicit-construction-from-the-E-type is desirable. `if (!cwd) return
cwd.error()` is easier on the eyes than `if (!cwd) return
make_unexpected(cwd.error())`. However, I wonder whether we could get
`if (!cwd) return cwd.unexpected()`.
I should probably say here that I find the name "unexpected"
(or "expected", for that matter) particularly unhelpful.
In my world, there's nothing unexpected about an error,
and the discussion about similarities vs. std::variant
seems to indicate that T and E are sometimes viewed as being
on the same level (they're not; E is still an error).
Distinction without difference. "Error", "unexpected", "exceptional", and
"disappointment" are all words for the same thing.

Don't think of T and E as being "on the same level." There's a reason we
say "T and *E*" instead of "T and U" or "T1 and T2".
Expected<T,E> is similar to variant<T,E> in exactly the same way that
optional<T> is similar to variant<T,monostate>.
Post by Jens Maurer
Post by Arthur O'Dwyer
For the long-term problems
caused by implicit conversions involving vocabulary types, please see
the ongoing fiasco of std::string, std::string_view,
std::filesystem::path. Eliminating implicit conversions from your C++
code eliminates bugs. Eliminating implicit conversions from your
proposals eliminates literally YEARS of work for the future members
of the Committee.
It seems to me that some of that wrapping might cause extra copies
or moves to stick the "E" instance into the wrapper before passing
it to the std::result constructor. Also, I'd be happy to allow
only exact matches for E, i.e. you can only implicitly convert an E
to a std::result<T,E>, not to a std::result<T, E2>, where E converts
to E2. That seems a lot safer than the situation with std::string and
friends.
std::result<T,E> will be passed from function to function by value. (Unless
you like out-parameters? I should add a 14th point: I hate out-parameters.)
So we assume that both T and E are efficiently move-constructible. "Extra
copies" aren't a problem.
Also, I'm not suggesting any kind of new "wrapper" *type*; I'm just
suggesting that conversions between the existing types be *explicit*
instead of happening implicitly.
The Expected proposal includes a wrapper type std::unexpected<E>, but it's
exactly as lightweight as E itself, and has move semantics, so there are no
extra copies happening.
Post by Jens Maurer
(10) Speaking of "levelization", I do like the idea of having
Post by Arthur O'Dwyer
"higher-level" and "lower-level" headers in the STL. I think it's
unfortunate that <system_error> — which sounds like a low-level
header — actually depends on <string>, which recursively depends on
most of the STL (<string_view>, <ostream>, et cetera). I talked to
Charley about this at CppCon: I'm ambivalent. On the one hand I think
it really sucks that <system_error> has any dependencies at all. On
the other hand, I appreciate the idea of "vocabulary types," and it
feels hypocritical to condemn the library's own use of `std::string`
as the vocabulary type for returning a string.
It seems, since std::result<T,E> is a template, we can stick it into
its own header and only people wanting E = std::error_code need to
pull in <system_error> (and its dependencies) themselves. People
wanting E = their_own_enum will be fine as-is.
I agree with your general gist here, but specifically in this case I think
Niall wants std::error_code to be used in the default template arguments of
std::result. So for example we could write the source code

#include <result> // for std::result
int main() { std::result<int> x; auto y = x.error().message(); }

and expect that it would compile
<https://bugs.llvm.org/show_bug.cgi?id=34529>.
Header inclusion in the C++ library is a mess, though. I don't expect us to
"solve" it in this thread. :)
Post by Jens Maurer
Post by Arthur O'Dwyer
(12) Niall writes: "E is not necessarily an error for Expected. It's
merely an 'unexpected'." This is a distinction without a difference.
Whether we call it an "error", an "unexpected result", or an
"exceptional state", the point is that it's a "disappointment" (to
use Lawrence Crowl's term). The user's expectations are that
foo.value() will give them the normal-path result (if any), and that
foo.error() will give them the "disappointment-path" result (if any).
Jens points out, rightly, that if the user asks for foo.error() and
there is no disappointment to report, it makes some sense to return
"no error" as opposed to blowing up the program via a thrown
exception or via undefined behavior. As to the question of whether we
can generally consider a default-constructed E to indicate "no
error", please see my point (1) in this email.
I'm not convinced we should restrict E to the set of types Niall
has in his paper; if we have a more general E, it's maybe a harder
sell that a default-constructed E should never actually indicate an
error.
I am happy with Niall's logic earlier in this thread that the library's job
is to give the user a clear way to *not* shoot themselves in the foot. If
some user chooses to do something dumb like set E = std::mutex or whatever,
then the library does not need to coddle that particular user.
I am also happy to restrict the set of allowable E's right now and relax it
later. However, I certainly don't like Niall's particular choice of
restriction; I wouldn't involve is_base_of at all. Letting E = std::string
seems plausible, for example. (And std::string is cheaply
move-constructible.)

–Arthur
--
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposals+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/CADvuK0KxuoR2h_XFy6UgPz%2B3ewPumxAk4Da_w181NZDJpD_GTw%40mail.gmail.com.
Jens Maurer
2017-10-03 20:27:36 UTC
Permalink
Post by Jens Maurer
Post by Arthur O'Dwyer
(6) Jens's example use-case #4 almost convinced me that
implicit-construction-from-the-E-type is desirable. `if (!cwd) return
cwd.error()` is easier on the eyes than `if (!cwd) return
make_unexpected(cwd.error())`. However, I wonder whether we could get
`if (!cwd) return cwd.unexpected()`.
I should probably say here that I find the name "unexpected"
(or "expected", for that matter) particularly unhelpful.
In my world, there's nothing unexpected about an error,
and the discussion about similarities vs. std::variant
seems to indicate that T and E are sometimes viewed as being
on the same level (they're not; E is still an error).
Distinction without difference. "Error", "unexpected", "exceptional", and "disappointment" are all words for the same thing.
Right, but it seems I have to write one of these words over
and over again in my future code, so from that angle, I care.
Post by Jens Maurer
std::result<T,E> will be passed from function to function by value. (Unless you like out-parameters? I should add a 14th point: I hate out-parameters.) So we assume that both T and E are efficiently move-constructible. "Extra copies" aren't a problem.
Also, I'm not suggesting any kind of new "wrapper" /type/; I'm just suggesting that conversions between the existing types be /explicit/ instead of happening implicitly.
The Expected proposal includes a wrapper type std::unexpected<E>, but it's exactly as lightweight as E itself, and has move semantics, so there are no extra copies happening.
Not all moves are cheap. std::array<int, 1024> is not cheap to move.
Post by Jens Maurer
I am happy with Niall's logic earlier in this thread that the library's job is to give the user a clear way to /not/ shoot themselves in the foot. If some user chooses to do something dumb like set E = std::mutex or whatever, then the library does not need to coddle that particular user.
I am also happy to restrict the set of allowable E's right now and relax it later. However, I certainly don't like Niall's particular choice of restriction; I wouldn't involve is_base_of at all. Letting E = std::string seems plausible, for example. (And std::string is cheaply move-constructible.)
I don't think we should get into the business of trying to
enumerate the plausible E's here; this would be a large time-sink
for no good reason. (Prescribing some properties of E such as
"default constructible" is fine, though.)

Jens
--
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposals+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/59D3F2B8.6060404%40gmx.net.
Niall Douglas
2017-10-03 21:52:14 UTC
Permalink
Post by Jens Maurer
There's a union where an "unsigned int" is shared with E; this should
simply go. If you want to show standard-layout, just show
struct result {
T _value;
/* integral or so */ discriminator;
E _error;
};
However, prescribing this order of members might be sub-optimal in
terms of alignment / packing of T and E; some other order might
yield a smaller "result" layout on some platforms for some T and E.
This is the struct in interim draft 2 paper:

struct
{
T value;
// flags bit 0 set if value contains a T instance (and E is to be ignored)
// flags bit 1 set if value does not contain a T (and E is to be observed)
// flags bit 4 set if error is a generic POSIX errno int
(std::generic_category, std::errc enum)
unsigned int flags;
E error;
};

Assuming that 99% of the time that E will either be a std::error_code
(struct { int code; void *category; }) or some enum, the above layout on 32
bit should be optimal much of the time as the unsigned and int ought to
pack together.

On 64 bit, the error code will pad to 8 bytes, and there is wastage. But I
intentionally chose for 32 bit to pad well as I figured it more important
there.

Now I consider it, I can't actually drop the union around E error after
all, it loses the padding on 64 bit systems. I think I'll go use the actual
implementation in Outcome which is confusing, but correct.

I sympathize, but I still feel allowing something custom
Post by Jens Maurer
such as
struct my_E {
std::error_code ec;
std::filesystem::path failed_path;
std::string failed_function;
};
is the way to go here. If, on your way out, you need to strip
that info down (maybe after logging) to E = std::error_code, you
just pick the relevant member.
That would break TRY which needs type E to be consistent across all
functions.

Lest you think that TRY not be so important, I can assure you that it is an
*enormous* boilerplate saver in real world code using these objects.
Sufficiently a boon in fact that you'll deliberately choose in a code base
of any size or complexity a single type E for your entire program.

In Outcome v2, this is implemented via a macro OUTCOME_TRY or the superior
OUTCOME_TRYX if you are on GCC or clang. For WG21, I am submitting the
paper "D0779R0: Proposing operator try()" with Vicente.
Post by Jens Maurer
(I have a feeling that creating
a std::exception_ptr is non-cheap, so requiring people to transport
extra info via that mechanism seems not a good idea.)
Constructing one costs about 3000 to 5000 CPU cycles.

Niall
--
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposals+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/32b0f9d0-5258-4794-b668-d3c17441ecc7%40isocpp.org.
Niall Douglas
2017-10-03 21:33:23 UTC
Permalink
Post by Arthur O'Dwyer
(3) It seems very important to Niall that the Expected/Result type that is
standardized should have a normatively defined class definition, so that
anyone could look at the Standard wording and deduce exactly how to
interoperate with Expected/Result from C code or from pre-C++2a code. I
sympathize with this idea. I think Nicol was right when he said that the
C++ Standard generally does *not* standardize class definitions. So just be
aware that you will be swimming upstream to get this — especially if you
want a non-standard-layout class with special member functions, private
members, etc etc. Jens gives the example of std::complex.
All I request is that standard layout-ness is preserved. So if T and E are
both standard layout, then so must be result<T, E>.

Outcome v2 already implements this guarantee.
Post by Arthur O'Dwyer
(5) I still believe that it is a sin to subclass std::error_code,
std::exception_ptr, or any other std:: class type that is not already part
of a classical hierarchy. Composition/aggregation is okay. Inheritance is
not. You will get slicing and you will get bugs.
I think it's entirely fair that if an end user controls his entire program
world that he can create his own local error_code subclassed from
std::error_code with added payload. Ditto for exception_ptr.

Sure, private inheritance would be better, but that's up to the end user.

For the record, Outcome v1's error_code_extended was safe to slice. You
lost the payload, but it was safe.
Post by Arthur O'Dwyer
(6) Jens's example use-case #4 almost convinced me that
implicit-construction-from-the-E-type is desirable. `if (!cwd) return
cwd.error()` is easier on the eyes than `if (!cwd) return
make_unexpected(cwd.error())`. However, I wonder whether we could get the
best of both worlds by providing a convenience member function: `if (!cwd)
return cwd.unexpected()`. For the long-term problems caused by implicit
conversions involving vocabulary types, please see the ongoing fiasco of
std::string, std::string_view, std::filesystem::path. Eliminating implicit
conversions from your C++ code eliminates bugs. Eliminating implicit
conversions from your proposals eliminates literally YEARS of work for the
future members of the Committee.
I believe D0323R3 just removed the expected.get_unexpected() just this
revision. And make_unexpected was removed in Toronto incidentally, along
with all the make_* functions. They're all gone now.

See https://github.com/viboes/std-make/blob/master/doc/proposal/expected/d0323r3.md
Post by Arthur O'Dwyer
(8) I certainly hope that both Result<T,E> and Expected<T,E> are "X"
whenever both T and E are "X", for values of "X" including at least
"trivially copyable" and "trivially destructible". If some implementation
of Result or Expected does not provide those guarantees, then that's a QoI
issue against that implementation. (I hope that the version that gets
standardized will have normative wording *requiring* those guarantees. If
that happens, then implementations without those guarantees will be
non-conforming.)
Both Outcome and Expected preserve triviality in copy, move, assignment and
destruction. Both provide strong never empty guarantees. Currently only
Outcome preserves standard layout-ness, but Vicente is open to Expected
also doing so if Albuquerque likes the idea.
Post by Arthur O'Dwyer
(9) Niall wrote: "If you are adding a feature which uses 98% of an
existing feature, then the standard should require use of that existing
feature." I strongly disagree with that statement. Now, I *do* think (and
I think Niall might agree) that we should not add new features with small
gratuitous quirks that make them impossible to implement in terms of other
std::packaged_task<R(A...)> is not implementable in terms of
std::function<R(A...)> because copyability, and std::vector<T,A>::resize is
not implementable in terms of std::uninitialized_move_if_noexcept() because
allocator_traits. Therefore, if it were truly *impossible* to implement
Expected<T,E> in terms of std::variant<T,E>, I'd complain.
HOWEVER, I repeat that no sane implementation should pull in <variant> as
a result of including <expected>. The former is a "higher-level" feature
than the latter; it includes a ton of TMP machinery that is not needed by
<expected>. I might expect both headers to include some "helper" header
providing just the common parts needed by both.
This is *exactly* what I want to see: <basic_variant>
Post by Arthur O'Dwyer
We see an example of this in both libstdc++ and libc++, where <set> and
<map> are implemented in terms of a "helper" header sometimes named
<__tree>. It is theoretically possible to implement std::map<K,V> in terms
of std::set<something_clever<K,V>>, but no sane implementation will pull in
all of <set> as a result of including <map>.
I hope this clarifies my position.
Dinkumware's STL is also full of common internal implementation classes
shared by the public ones.

What bugs me severely is that frequently I am writing code which needs a
*subset* of a STL container, and lo and behold haven't all three major STL
implementations found the exact same problem and have internal classes
implementing *exactly* what I need.

Those internal classes should be standardised into C++! Those internal
classes are often a damn sight more useful than the official STL classes,
but they're usually made private costing me and countless others effort in
reinventing a wheel reinvented many times already.
Post by Arthur O'Dwyer
(10) Speaking of "levelization", I do like the idea of having
"higher-level" and "lower-level" headers in the STL. I think it's
unfortunate that <system_error> — which sounds like a low-level header —
actually depends on <string>, which recursively depends on most of the STL
(<string_view>, <ostream>, et cetera). I talked to Charley about this at
CppCon: I'm ambivalent. On the one hand I think it really sucks that
<system_error> has any dependencies at all. On the other hand, I appreciate
the idea of "vocabulary types," and it feels hypocritical to condemn the
library's own use of `std::string` as the vocabulary type for returning a
string.
The reason it depends on string is that Beman felt that message strings
would be generally programmatically, and therefore need memory allocation
and freeing once done.

And indeed on Windows they are generated programmatically, localised to the
current user language.
Post by Arthur O'Dwyer
(11) Niall writes: "Outcome uses struct storage, not union storage."
The paper Jens and I were reading flatly contradicts that statement, on
page 4, where it says "Result requires the following layout to be
implemented:" and then shows a piece of code involving a union. However,
the reference C implementation
on page 14 uses a non-union struct, and the reference C++ implementation
on page 17 uses a "non-standard" optional<T> with the caveats listed in a
comment at the bottom of page 16. It would be much *much* clearer if the
new paper didn't imply the use of a union/optional/variant; it would save
Niall a lot of argumentation. If you want a standard-layout struct, just
use one! Don't even put "union" in your sample code!
Others also found draft 1 confusing. Outcome v2 has always used struct
storage, it was a recommendation by the peer review. I've uploaded an
interim draft 2 paper on another thread to hopefully clarify this problem.
Post by Arthur O'Dwyer
(12) Niall writes: "E is not necessarily an error for Expected. It's
merely an 'unexpected'." This is a distinction without a difference.
Whether we call it an "error", an "unexpected result", or an "exceptional
state", the point is that it's a "disappointment" (to use Lawrence Crowl's
term). The user's expectations are that foo.value() will give them the
normal-path result (if any), and that foo.error() will give them the
"disappointment-path" result (if any). Jens points out, rightly, that if
the user asks for foo.error() and there is no disappointment to report, it
makes some sense to return "no error" as opposed to blowing up the program
via a thrown exception or via undefined behavior. As to the question of
whether we can generally consider a default-constructed E to indicate "no
error", please see my point (1) in this email.
I thought Jens was making a stronger point than this actually. He seemed to
feel that these objects aren't returning disappointment, they are returning
an *error*. That's a much stronger meaning than *failure*, let alone
disappointment.

And I'm fairly much in agreement on that. Draft 2 paper tries to get into
the importance of starting your design with a clearly defined mission and
use case. Lots of "tricky" decisions fall into place if everyone is onboard
regarding what problem we are solving.
Post by Arthur O'Dwyer
(13) I don't see the usefulness of outcome<X,Y,Z>; but I know that if I
don't like it I can avoid using it, and it sounds like Niall is not
proposing that one for standardization anyway. (That is, he's proposing it
for Boost.Outcome but not for C++2a.)
outcome<T, EC, E|P> is definitely a Boost-ism. As are the policy classes,
ADL injection points, and all that other stuff Boost folk love. I only
mentioned it to demonstrate there are design layers of increasing
complexity in Outcome, that's all. A subset of those might be better to
standardise than one single object design to rule them all.

Niall
--
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposals+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/7e4b7d7d-a3ae-42c0-930b-f53131f1d7c2%40isocpp.org.
Niall Douglas
2017-10-03 21:08:04 UTC
Permalink
Post by Jens Maurer
Now, it's obviously hard to argue about C compatibility for
something that is a template and has a plethora of interesting
constructors. However, we already attempt something like
that for std::complex; see 29.5 p4 [complex.numbers].
Maybe we want to say something like std::result and a struct X
consisting of T, unsigned int, and E members have a common
initial sequence that is all of X.
The way I phrased it is that Outcome propagates standard layoutness of T
and E. So if both T and E are standard layout, result<T, E> will also be
standard layout and and it will be this C struct X here.

Niall
--
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposals+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/a86e07d0-c3b1-430e-b725-2a281f4a862f%40isocpp.org.
Loading...