C++ Logo

std-proposals

Advanced search

Re: Yet another member function for std::map

From: Arthur O'Dwyer <arthur.j.odwyer_at_[hidden]>
Date: Wed, 28 Jul 2021 21:39:27 -0400
On Wed, Jul 28, 2021 at 8:29 PM Barry Revzin <barry.revzin_at_[hidden]> wrote:

> On Wed, Jul 28, 2021 at 2:40 PM Arthur O'Dwyer via Std-Proposals <
> std-proposals_at_[hidden]> 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:
>>
>> 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>())) { ... }
>

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
 https://godbolt.org/z/fMvb1Gs3a

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

Received on 2021-07-28 20:39:40