Date: Wed, 28 Jul 2021 19:29:45 -0500
On Wed, Jul 28, 2021 at 2:40 PM Arthur O'Dwyer via Std-Proposals <
std-proposals_at_[hidden]> wrote:
> On Wed, Jul 28, 2021 at 3:20 PM Edward Catmur via Std-Proposals <
> std-proposals_at_[hidden]> wrote:
>
>> On Wed, 28 Jul 2021 at 20:16, Kyle Knoepfel via Std-Proposals <
>> std-proposals_at_[hidden]> wrote:
>>
>>>
>>> Right...which is why I said the return type of value_for(...) would not
>>> be quite like std::optional. So there seem to be four or five options to
>>> me:
>>>
>>> 1. std::map<K, V>::value_for(K const&) -> typename std::map<K,
>>> V>::value_handle, where map gains a nested type that behaves like
>>> std::optional<V&> would behave. Yuck.
>>> 2. std::map<K, V>::value_for(K const&) -> V*, where a bare pointer, or
>>> some equivalent, is returned. Also yuck.
>>> 3. std::map<K, V>::value_for(K const&) -> std::optional<V&>. Requires
>>> convincing the standards committee to support reference-type template
>>> arguments for std::optional.
>>> 4. Something else I haven't thought of
>>> 5. Stop wanting this feature
>>>
>>> I think 3 is the ideal solution, but the most unlikely to occur. Option
>>> 1 is doable but I assume no one wants to add an additional type to
>>> std::map. I fear 5 is the most likely solution, although it would sadden
>>> me.
>>>
>>
>> 4. optional<reference_wrapper<V>>?
>>
>
>
optional<reference_wrapper<T>> is a very poor substitute for optional<T&>
in basically all respects.
> 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:
> 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:
>
> https://github.com/zenorogue/hyperrogue/pull/246/files#diff-f42f1abd2e52db6ab3f7da5cc6c2e290adc46e1d7cd47c673ea49f5ead738c96R851
>
> 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>())) { ... }
And this isn't a problem at all - we're just composing different APIs
together.
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?
With optional, these things just compose straightforwardly, so:
m.value_at(p).transform(ranges::size).value_or(0)
How do you do that with a pointer?
Note that neither of these examples work with
optional<reference_wrapper<T>> either.
> Which goes to illustrate what I think is my point: helper methods are
> super useful but also have way too many knobs to be worth standardizing.
> Just like type-erased callable wrappers, every codebase is going to want to
> make their own tradeoffs and control their own code.)
>
> my $.02,
> Arthur
>
The nice thing about optional is that it's optional that has the knobs, you
don't need to reinvent all of those APIs.
Barry
std-proposals_at_[hidden]> wrote:
> On Wed, Jul 28, 2021 at 3:20 PM Edward Catmur via Std-Proposals <
> std-proposals_at_[hidden]> wrote:
>
>> On Wed, 28 Jul 2021 at 20:16, Kyle Knoepfel via Std-Proposals <
>> std-proposals_at_[hidden]> wrote:
>>
>>>
>>> Right...which is why I said the return type of value_for(...) would not
>>> be quite like std::optional. So there seem to be four or five options to
>>> me:
>>>
>>> 1. std::map<K, V>::value_for(K const&) -> typename std::map<K,
>>> V>::value_handle, where map gains a nested type that behaves like
>>> std::optional<V&> would behave. Yuck.
>>> 2. std::map<K, V>::value_for(K const&) -> V*, where a bare pointer, or
>>> some equivalent, is returned. Also yuck.
>>> 3. std::map<K, V>::value_for(K const&) -> std::optional<V&>. Requires
>>> convincing the standards committee to support reference-type template
>>> arguments for std::optional.
>>> 4. Something else I haven't thought of
>>> 5. Stop wanting this feature
>>>
>>> I think 3 is the ideal solution, but the most unlikely to occur. Option
>>> 1 is doable but I assume no one wants to add an additional type to
>>> std::map. I fear 5 is the most likely solution, although it would sadden
>>> me.
>>>
>>
>> 4. optional<reference_wrapper<V>>?
>>
>
>
optional<reference_wrapper<T>> is a very poor substitute for optional<T&>
in basically all respects.
> 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:
> 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:
>
> https://github.com/zenorogue/hyperrogue/pull/246/files#diff-f42f1abd2e52db6ab3f7da5cc6c2e290adc46e1d7cd47c673ea49f5ead738c96R851
>
> 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>())) { ... }
And this isn't a problem at all - we're just composing different APIs
together.
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?
With optional, these things just compose straightforwardly, so:
m.value_at(p).transform(ranges::size).value_or(0)
How do you do that with a pointer?
Note that neither of these examples work with
optional<reference_wrapper<T>> either.
> Which goes to illustrate what I think is my point: helper methods are
> super useful but also have way too many knobs to be worth standardizing.
> Just like type-erased callable wrappers, every codebase is going to want to
> make their own tradeoffs and control their own code.)
>
> my $.02,
> Arthur
>
The nice thing about optional is that it's optional that has the knobs, you
don't need to reinvent all of those APIs.
Barry
Received on 2021-07-28 19:30:04