C++ Logo

std-proposals

Advanced search

Re: P2192R0 -- Transparent return type

From: Jason McKesson <jmckesson_at_[hidden]>
Date: Tue, 28 Jul 2020 20:32:20 -0400
== Editorial comments ==

The biggest editorial issue is stylistic. All of your paragraphs are
numbered, but they read like numbered lists. If your intent is to
number your paragraphs (and it's *really* unnecessary and distracting
to read through), then you need to reformat the PDF to make the
numbering look less like numbered lists (the way the standard does).

Coupled with this is the fact that you often just write one sentence
on a line, which makes it look a *lot* like you're writing a numbered
list, rather than actual paragraphs that can be referenced. This is
really distracting to read. The very first two items in the Abstract
are just two sentences of the same paragraph. It reads so much better
as a full paragraph:

> This is a proposal to introduce a behavioral pattern for optional but standard function return handling. It is an extremely simple type and behavior pattern of both producers and consumers using it.

The second sentence is simply providing details for the first. They
belong in the same paragraph, not on different numbered lines.

Lastly, if you're going to write in a style so reminiscent of a formal
standard, you should be a lot less conversational in the tone of your
writing (such as the use of epigraphs starting certain chapters).

On other points, you frequently use the word "concept" for the thing
you're defining. You use this word a *lot* long before we actually see
`valstat`. Coupled with how you often maligned other similar
value-and/or-error types for being too large, that gave me the
impression that you were about to unveil an actual C++20 "concept" for
such a type. You should make it much more clear that you're talking
about an actual type, not a C++20 constraint.

Also, you use the phrase "returns concept" or "returns handling
concept" in multiple places. This is... confusingly worded. I did some
Googling, but I couldn't find reference to such phrases anywhere else,
so it does not appear to be a term of art. My understanding is that
these terms broadly represent the general concept of a "value and/or
error/status" type. If that's the case, you need to make that much
more clear up front, or to use terminology that is more clear and
obvious to the reader.

You talk about how "meta-states" are an implied idea of `valstat`,
rather than being a part of its definition in any material way. That's
fine... except that right before that, you showed a large code-block
for the `state_combination` function that looks remarkably like C++
that outlines what those concepts would look like. If it is not your
intent for "meta-states" to be a formal part of C++ code, then you
shouldn't have a code block talking about them.

=== Some typos ===

* "adoption of this proposal should contributes, to all the three
'requirements' above." You mean "should *contribute*", singular.

* "Thus it can not solve the issue of constructors not throwing
extensions but needing to report the outcome." I think you meant
"exceptions", not "extensions".

== Technical Discussion ==

The question that was most on my mind after seeing what `valstat` was
after page-after-page of build-up was... ***why?*** And this question
has many parts.

=== Why Not The Alternatives? ===

I find your arguments for why one should not use `std::expected` (part
of Library Fundamentals v3) or `boost::outcome` to be... odd. Now, you
do mention a viable technical reason, but I'll get to that in a
minute. The less viable technical reason you bring up is that the
implementation of these types is long, which is bad.

But... why is that bad?

I'm not being asked to *write* `expected`, just as I'm not being asked
to write `std::vector` or `std::optional`. That doesn't mean that they
can have whatever bloated interfaces we want, but I don't find lines
of code to be a convincing reason not to use someone's type.
Especially in a post-Modules world where the compile time of such code
is a lot less relevant.

If you were defining an actual C++20 `concept`, then I could
understand having a discussion about what interfaces should be
required by users implementing said concepts vs. which interfaces
aren't worth the cost. But there is no C++20 concept; there's just
`valstat`. I don't see a reason to care that `valstat` is short while
`expected` is long; what I care about is how *functional* these types
are.

Also, it should be noted that many C++20 features will likely make
`expected` and `outcome` *much* shorter, and even easier to code. Many
of the tricks they have to do are workarounds for not having
constraints on non-template constructors, or dealing with `explicit`
constructors of various types. C++20 makes those much, much easier to
deal with.

=== Why One Size Fits All? ===

You make an interesting point with your "meta-state" idea. You make
the point that it is conceptually valid, in a general sense, for a
function to return any pairing of value and "error". Each combination
of these possibilities has a distinct meaning.

However, one thing that `valstat` doesn't take into account is... do
all functions actually provide all meta-states? If I'm writing a
function that either returns a value or an error, and I return
`valstat`, that heavily implies that the user should check all four
possibilities.

Wouldn't it make a lot more sense for such functions to return a type
that is *explicitly* restricted to being just Value or Error? That's
good self-documenting code practice, yes?

Indeed, if a function returns neither Value nor Error, in your
meta-state diagram, that constitutes "empty". But again, is that a
state that *every function* that returns an Error could possibly
yield? It's right there in `valstat`'s API, so if I receive one from a
function, that ought to tell me that I should assume it is a
possibility.

Basically, my point here is that there are a variety of Value+Error
circumstances, and having a single type to handle them all is
basically making your API lie to the user.

To me, the only advantage of this one-size-fits-all solution is that
you get to use structured binding to store and unpack the results
(ValueOrError types can't use structured binding). But even that is
pretty flimsy, all things considered; it just makes it slightly easier
to access the values.

=== Why Status is Error? ===

This is another design aspect I find odd. If there's a Value in the
`valstat`, then the status code is a Status value. If there is no
Value, then the status code is an Error.

To me, these represent two very distinct pieces of information. Yet
they are required to have the *same type*. Why?

If I want to transmit a status code with a value to represent
completion with added information, then I ought to put the status code
inside of the value. That is, my Value type would be a
`pair<RealValue, StatusCode>` or similar. This gives me the
flexibility to make the status code a different type. Error types
might have significant information in them, but status codes don't
have to. Or if they do, it's very *different* information.

=== Why So Minimal? ===

A good lingua franca type, in my mind, has two very important properties:

1. It provides a common type for frequently-used idioms. This permits
interoperation between code, as they both use the same type for the
same kind of thing.

2. It provides useful functionality in and of itself.

Even if `valstat` solves #1, it fails at #2. The type provides
*absolutely nothing* in terms of actual functionality in and of
itself.

To understand what I mean, consider the case of returning an error. In
your type, you have to say `return {{}, ErrorValue};`. For a user to
understand that, they have to remember an idiom.

For `expected`, it looks like this: `return unexpected(ErrorValue);`.
This is so readable that you can literally read it aloud and
understand what it means: "return an unexpected error value". There
are no spurious braces or other unnecessary noise. It says exactly
what it does; no idiom needs to be remembered.

Basically, those thousands of lines of code in `expected` and
`outcome` have a *purpose* to them. They're not noise.

We don't write lingua franca types just for interoperability. We write
them because they're *useful* types. `string_view` provides useful
utility in and of itself outside of interoperability. You can
`string_view` a string literal, thus preserving its length and not
allocating storage, for example. And you still get a lot of
`basic_string`'s string manipulation API.

If we were following your paradigm, `string_view` would just be a
struct with a public `const CharT *` and a public `size_t`. No special
constructors, no range members, no string manipulation members, just a
pointer and a size. It would only ever see use as an interoperability
type, not as an actual, functional type.

Even ignoring everything else I said about `valstat`, if you feel that
the overall design of two optionals is good, there needs to be *more*
to the API than just a pair of optionals. Why not provide a
visitation-like mechanism that makes the four meta-states something
real rather than an idea? That'd probably work great with various
inspection proposals in the works.

Why not develop an actual *C++ concept* for these kinds of things?
That way, the pointer version you suggest at the end of your paper can
work with code that uses the concept too.

If I'm writing code by myself, I would never use `valstat`. It
provides nothing that I couldn't do myself almost as easily.

Received on 2020-07-28 19:35:49