On Wed, Jul 28, 2021 at 8:39 PM Arthur O'Dwyer <arthur.j.odwyer@gmail.com> wrote:
On Wed, Jul 28, 2021 at 8:29 PM Barry Revzin <barry.revzin@gmail.com> wrote:
On Wed, Jul 28, 2021 at 2:40 PM Arthur O'Dwyer via Std-Proposals <std-proposals@lists.isocpp.org> wrote:
 
2, "return bare pointer," is correct.

optional<T&> is a much better choice. Semantically, it matches the functionality better (you want either a T& or nothing, which is exactly the semantics of optional<T&>, while T* is more ambiguous) and the API of optional<T&> is just much better for this problem than the API of T*, in terms of ergonomics for the user. As is very clear from your example:

Meh, I would argue that what you want is either the address of a T or something-which-is-falsey-and-also-not-a-T, which is exactly what `T*` gives you (because null pointers).
Historically this user interface has been so abundantly successful that when C++17 adopted `std::optional`, they pasted the whole "pointer" interface onto it, so that e.g. you can write `o->foo()` instead of `o.value().foo()`.

The STL already uses this idiom in `std::get_if` and `std::any_cast`, as well as `std::dynamic_cast<T*>`.
Actually, many industry codebases already have a convenience function along these lines. Serendipitously, I just added one to HyperRogue the other day:

However, notice that there are many operations that you'd still want to do via third-party convenience functions. For example, in the exact same HyperRogue patch, I also introduced a helper function `span_at`, with these semantics:

template<class Map, class Key, class T = /*metaprogramming*/>
span<const T> span_at(const Map& map, const Key& key) {
    auto it = map.find(key);
    return (it == map.end()) ? span<const T>() : span<const T>(it->second.data(), it->second.size());
}

This is useful for things like
    const std::map<Parent, std::vector<Child>> m = ...;
    Parent p = ...;
    for (const Child& c :  span_at(m, p)) { ... }
where a naïve
    for (const Child& c : m[p])
would try to insert into `m`.
(And yes there's prior art for this API in optional::value_or. But `value_or(m, p, span<T>())` would have been much too verbose, and would have had trouble with type-checking, so for this codebase we want the shorter simpler version.

If you had a member function value_at that returned an optional, this would be:

for (child Child& c : m.value_at(p).value_or(span<T>())) { ... }

Sure, that's the option I presented in the previous sentence and said
- it would have been much too verbose: compare
 for (const Child& c : hr::span_at(m, p)) { ... }
 for (const Child& c : m.value_at(p).value_or(std::span<Child>())) { ... }
- it would have had trouble with type-checking: `vector` is convertible to `span`, but is not literally the same as `span`, so .value_or won't compile it

What would you do if value_at returned a T*? You can't do value_or() on a T*, because pointers have very few operations you can do on them (and nearly all of those would be straight up invalid in this use-case - which does not make for a great API!) you'd either push for some language feature that does that or you'd write some non-member function that handles this case. Which would be fine for value_or(), but not for any number of other operations that work for optional but not for pointers. What if you wanted the size of the span there, or 0?

Right — it sounds like you're agreeing with my point.
There are just too many things you could imagine doing with a helper function here.
 for (const Child& c : hr::size_at(m, p)) { ... }
or
 for (const Child& c : m.value_at(p).transform(std::ranges::size).value_or(0)) { ... }
(FWIW, this requires grafting monadic `transform` onto `std::optional` — but that's https://github.com/cplusplus/papers/issues/112 , likely to happen in C++23 anyway)
And then what if you wanted "the integer at that key position, if it exists, but only if it's even"? You could graft monadic `filter` onto `optional`... or, again, you could write a helper function. There will always be functions like this. You can write them yourself as free functions, or you can graft them onto `map`; or you can graft them onto `optional`; or you could even invent a new sort of `monadic_return<T>` that you can graft arbitrary functions onto, without choosing `optional` specifically to be that type. But (1) the stream of functions and knobs is never-ending, and (2) I'd rather write `size_at` than `value_at().transform(ranges::size).value_or(0)` any day.

–Arthur

It's not really relevant that you prefer size_at to value_at().transform(ranges::size).value_or(0).

For those people that prefer writing small functions and then writing programs that are built on composing those functions, returning optional<T&> from value_at() actually allows that style to be even possible, because optional<T&> composes while T* does not. Similarly to how ranges compose, but iterators do not. The advantage to such a compositional style is that there really is not a never-ending stream of utility functions, and once you learn the ones that do exist, you can build a combinatorial explosion of functionality. It's easy to read the latter expression at a glance and know exactly what it means (whereas size_at, while clearly suggesting that it returns the size of something, does not suggest a zero as a default). And any slight variation on any of these operations, I can also do, pretty easily. The amount of functionality that I get without writing new functions is extremely large. Moreover, the relative benefit from adding new functions to Optional ends up being quite large - both because of the number of different ways you can use them, and also because basically every member function you could add to Optional is pretty trivial, there's basically no implementation complexity to speak of.

For those people that prefer writing bespoke functions to solve problems, as you apparently do, then it doesn't even matter what value_at() returns. Or even that value_at(), or any other member function that users might find value from, exists. This certainly isn't any kind of argument for T* as a return type.

Also, I don't know if the filter example was supposed to be a gotcha!!, but you can absolutely do a filter using and_then() straightforwardly enough (and you of course cannot do this with T*, just like you cannot do any of these things with T*). I don't have a filter on my optional since I guess this particular operation hasn't come up much, but it's certainly not unreasonable to add (and a very easy function to implement) - and certainly not at the scale of gotcha. Besides, what is even the argument here? Optional doesn't have filter, therefore optional is useless? Optional does not solve every possible problem, therefore we should return T*, which solves no problem? Adding filter to optional is a really easy addition that extends our toolkit. Meanwhile, are you going to write size_at_if() and span_at_if() if you end up needing that functionality on top of all of your other non-composable operations? This is a pretty clear win on optional.

Utility types having a bunch of member functions is a good thing - they're utility types, we should care about people's user experience. Rust has 27 member functions on its Option (including filter). That's not a bad thing. And hardly never-ending.

Barry