On 30/10/2019 18:03, Zach Laine wrote:
Returning end of range on failure is incredibly inconvenient (for the
consumer; granted it's usually more convenient for the algorithm
implementer), and I'd be happier if STL algorithms didn't do that
either.
I see that as an unfortunate consequence of using generic iterators as
input parameters and return types, and not an otherwise desirable
design choice.
(ie. the STL algorithms do it because they couldn't do anything better.
string doesn't do it because it can do something better [since it knows
the iterator type and class, and can consequently choose to return
something other than an iterator].)
I heartily disagree, but I'm also very curious about this. As an
example, could you take one of the simple std algorithms (std::find
would be a very simple candidate), and show its definition in the style
you have in mind?
Ok: wall of text incoming.
There are many options (especially when you have Outcome and/or ranges),
but a very straightforward update using only existing STL concepts would
be to take this:
template
InputIt find(InputIt first, InputIt last, T const& value);
And change its return type:
template
optional<InputIt> my_find(InputIt first, InputIt last, T const& value);
It now either returns an iterator in the range [first, last) or it
returns nullopt. It will never return last.
This simple change means that code following this can easily detect
search failure without having to refer back to the actual input
parameters (ie. having to know the value of last).
This can be useful when searching a subset of the container (you don't
need to save a separate copy of "list.begin() + 5" just so that you can
use it as both the last parameter and for failure checks -- or worse,
write it twice and risk bugs if someone edits one but not the other).
It also can be useful when the container as a whole is a temporary
rvalue -- which is *rarely* useful for std::find (since after all a
successful iterator return is almost useless if the container itself is
about to be destroyed), but not completely so. Sometimes you only want
to detect existence without needing to actually consume the iterator.
Sometimes you can use the successful return iterator within the same
whole-expression, so the container's lifetime hasn't ended yet.
As for "last" itself, while having to pass both begin and end iterators
*usually* precludes using rvalue containers, that's also not always
true. Some containers/iterators will accept a default-constructed
iterator to mean "the end of the sequence" (especially when underlying
storage is a linked-list, but other containers/iterators use this model
too). This means that "first" can be passed "method().begin()" or some
other rvalue and "last" can be passed "iterator()". And in range-based
algorithms, there's only a single parameter to worry about anyway, so
it's even more likely.
As a demonstration, let's imagine a simple span-based contains check (or
replace span with your better range type of choice) -- it doesn't care
about the iterator other than to assure that there was a successful return:
template<typename T>
bool contains(std::span<T> range, T const& value)
{
return my_find(range.begin(), range.end(), value).has_value();
// or you can just cast to bool if you prefer
// alternatively, if my_find itself took a range:
return my_find(range, value).has_value();
}
This seems more readable (and less error prone) than an explicit "==
range.end()" check. And, given the range-based version of my_find, you
could even have code that does this:
if (my_find(method(), 42)) { /* method included 42 */ }
Or this:
return resolve(my_find(method(), 42), 0);
Where resolve(x, y) returns "x ? **x : y" -- somewhat like value_or, but
including the iterator dereference.
(That makes more sense with a map find, or predicate find, or where the
element type is a class that only checks key equality (so the above
would return a fully populated object if it exists or a default object
if not). Obviously it's a bit silly with plain ints, but you get the idea.)
Another extension might be to define my_find_value, which returns
optional<T> rather than optional<InputIt>. That doesn't allow mutating
the found value in the collection (so it can't replace my_find) but it's
still useful in read-only scenarios (and rvalue containers will almost
always be read-only scenarios), so that the above no longer needs
resolve and would become:
return my_find_value(method(), 42).value_or(0);
Of course, there are performance consequences of using rvalue
containers, since you're copying and throwing away data. But sometimes
it makes sense, for known-cheap containers or where the method wants to
do a lock-and-return-copy or dequeue or similar, but you're only
interested in part of the information.