Boost logo

Boost :

From: Neil Groves (neilgroves_at_[hidden])
Date: 2024-12-21 12:53:57


On Sat, 21 Dec 2024 at 12:11, Peter Dimov via Boost <boost_at_[hidden]>
wrote:

> Christian Mazakas wrote:
> > I'm with Andrey, I think the iterator API *is* the useful API.
> Typically, when I
> > find something in a map, I'll either erase or do something else where
> reusing
> > the iterator is a key part of everything.
> >
> > My whole thing was about what users actually wanted vs all the costs of
> > everything else.
>
> What this user has always wanted is to not have to write this:
>

> auto it = map.find( key );
> if( it != map.end() )
> {
> // do something with it->second
> }
>
> That's why I'm always defining a helper function `lookup` that returns a
> pointer, which allows me to write this instead:
>
> if( auto p = lookup( map, key ) )
> {
> // do something with *p
> }
>
> If there's a built-in way to obtain an optional<mapped_type&> instead
> of an iterator, I can use that instead of `lookup`, without changing the
> syntax.
>

What I tried to do was provide that with map.equal_range(key) |
mapped_value | invoke(fn). And then tidy the syntax, but I accept that
wasn't an outstanding success! I collapsed the map.equal_range and
mapped_value into one step and then we had map | mapped_values(key) |
invoke([](...) {})

In the end what I had for you paraphrasing you snippet above would be:

map | mapped_values(key) | invoke([&](auto&& p) {
            // do something with p
        };

Obviously here I've pulled the "if" into the boilerplate code and
eliminated the need for p to be optional. I like eliminating the
possibility of a dereference of nullopt and I like not having to include
either implementation of optional. If we need to have work in the "else"
path then this is less awesome and projection to optional is one approach
to tackle this. Where we would like to pick a replacement default value
when missing that could probably also use syntactic sugar to replace the
invoke. The composition possibilities looked interesting to me, and I
thought that the common cases could appear approximately as the expressed
desired syntax of other approaches with a little syntactic sugar.

I was always open to name changing, so mapped_values could be "lookup" if
you wanted. Of course I could go a step further and collapse those two
adapters. We also discussed what would be returned, and of course it would
be possible to return an optional single element where appropriate. I like
the generality of passing the range of values back, especially since that
works for multi containers. The optional, to me, looks to be an inferior
alternative for an empty or not range, for many of the cases. Perhaps more
real world examples would change my mind. Nonetheless I could give the
projection to optional no trouble at all. Hence, in the end, I thought I'd
pretty much got every issue I'd heard addressed. I realize though that
small details to me may be much more important to others. I don't typically
confuse my opinion with fact. I'm definitely not young enough to know
everything.

I liked not having the explicit if, but totally accept that before it
becomes familiar it may look odd. It also takes away the possibility of
incorrectly dereferencing a nullopt, while apparently providing a very
similar solution to the expressed problem. One of the posted suggestions
used the optional map function to get the same advantage.

Thus I went on this journey to see if we could just keep the ranges we
already had and get the syntax close to the original expressions. It looked
pretty close to me, but it didn't appeal, which is totally fine. It would
be boring if we all agreed all the time.

I thus don't have anything concrete to merge that addresses the original
poster's request, which is disappointing but totally okay from my
perspective.

>
> Although it strikes me that it should probably be named `try_at` instead
>

Totally with you on "try_at" versus "try_find", but if we use 0..1 ranges
it is just equal_range, that's mapped_value adapted.

> of `try_find`. `find` is already a try-ing member function, because it
> doesn't
> fail, whereas `at` both returns the correct reference type, and can fail.
>
> Of course `try_at` by convention ought to return boost::system::result
> or std::expected instead of an optional, but that's another story.
>
> Interestingly, and maybe relevant for this discussion, `at` was added to
> associative containers even though it could have been made a free
> function instead.
>

I've looked at the recent standard changes and to me it looks like we are
semi-randomly preferring member and non-member functions in an
inconsistnent and arbitrary manner. We also added std::erase_if as a
non-member for std::vector and other containers. I would estimate that you
are probably aware of many more of these changes than I am. They've gone in
both directions. To me it seems we can't use recent history to inform us. I
accept that it may appear arbitrary and inconsistent because I have failed
to comprehend the rules that drive the final decisions.

Apologies if by asserting that I've achieved nothing this seems to be an
emotional reaction. It isn't. While I'm disappointed I couldn't help with
the original problem. I consider this eventuality to be part of the
acceptable, expected set of outcomes from the effort. If it weren't then
I'm not taking enough risk.

My perception is that my suggestion doesn't have broad appeal and hence I'm
not pushing to make it happen. Other solutions can be explored, or we can
leave it as it is. I never intended my experimentation to block progress
for other approaches. I also never intended to make any demands about how
anyone approached the problem.

I'm happy to continue to help, if I am actually doing so, but equally happy
to get out of the way and let others come up with something else.

Regards,

Neil Groves


Boost list run by bdawes at acm.org, gregod at cs.rpi.edu, cpdaniel at pacbell.net, john at johnmaddock.co.uk