
On Sat, 21 Dec 2024 at 12:11, Peter Dimov via Boost
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
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