3

In Java, Stream.findFirst() returns Optional<T>. I'd like to have the similar behavior for std::ranges::find(). If the value is not found, it returns last iterator. This is inconvenient if T is a struct and I try to get a member out of it. Here is a demonstration code:

    struct Person {
        int age;
        int height;
    };

    std::vector<Person> people;
    people.emplace_back(20, 100);
    people.emplace_back(23, 170);

    // find the height of a 23yo person
    std::optional<int> height1 = std::ranges::find(people, 23, &Person::age)->height;

    // find the height of a 26yo person
    std::optional<int> height2 = std::ranges::find(people, 26, &Person::age)->height;  // error

Of course I can put some wrapper code around each find to transform the iterator, but it makes the code so verbose and boilplate-y. I wonder if there's some more idiomatic way in C++20 to do this?

    std::optional<int> height2;
    auto iter = std::ranges::find(people, 26, &Person::age);
    if (iter == people.end()) {
        height2 = std::nullopt;
    } else {
        height2 = iter->height;
    }
Reci
  • 4,099
  • 3
  • 37
  • 42
  • 1
    You can always write a wrapper function that encapsulates the check for `.end()`. No need to duplicate the code everywhere. – Ken Wayne VanderLinde Dec 20 '20 at 20:09
  • Thinking about it, I actually want a wrapper that 1) transform from `borrowed_iterator` to `std::optional`, and 2) allow a projection to extract member from `T`. Can you provide an templated example of such wrapper in answer? – Reci Dec 20 '20 at 20:40
  • [Anyway, `op*` and `op->` simply assume they are used correctly on pain of UB.](//en.cppreference.com/w/cpp/utility/optional/operator*) Yes, `!result` is a bit more convenient than `result == std::end(container)`, but that balances against direct use of the iterator. Also, an optional iterator is often bigger than a simple iterator... – Deduplicator Dec 21 '20 at 01:04

2 Answers2

1
template <class ...ArgsT>
auto OpRangesFind(auto &&Range, ArgsT &&...Args)
{
    auto Iter = std::ranges::find(Range, std::forward<ArgsT>(Args)...);
    return Iter != Range.end() ? Iter : std::optional<decltype(Iter)>{std::nullopt};
}

int main()
{
    /* ... */

    auto OpIter = OpRangesFind(people, 26, &Person::age);
    if (!OpIter.has_value()) {
        std::cout << "not found\n";
    } else {
        std::cout << "found\n";
        std::cout << "height: " << OpIter.value()->height << "\n";
        //                               ^^^^^^^^
    }
}
Sprite
  • 3,222
  • 1
  • 12
  • 29
0

Here's my own solution to write a generic wrapper. Comparing to Sprite's version, my version returns member variable instead of iterator.

template<std::ranges::input_range R, class T, class ProjFind = std::identity, class ProjOut = std::identity>
requires std::indirect_binary_predicate<std::ranges::equal_to,
                                        std::projected<std::ranges::iterator_t<R>, ProjFind>,
                                        const T *>
auto optionalFind( R&& r, const T& value, ProjFind projFind = {}, ProjOut projOut = {} ) {
    const auto iter = std::ranges::find(r, value, projFind);
    return iter == r.end() ? std::nullopt : std::optional(std::invoke(projOut, *iter));
}

And use it with

std::optional<int> height2 = optionalFind(people, 26, &Person::age, &Person::height);

or

std::optional<Person> person = optionalFind(people, 26, &Person::age);
Reci
  • 4,099
  • 3
  • 37
  • 42