[libcxx-commits] [PATCH] D136522: [libcxx] patch for implementing ranges::find_last
Konstantin Varlamov via Phabricator via libcxx-commits
libcxx-commits at lists.llvm.org
Fri Jun 2 19:12:49 PDT 2023
var-const requested changes to this revision.
var-const added a comment.
This revision now requires changes to proceed.
So sorry it took me so long to get to this review!
================
Comment at: libcxx/include/__algorithm/ranges_find_last.h:15
+#include <__functional/identity.h>
+#include <__functional/invoke.h>
+#include <__functional/ranges_operations.h>
----------------
Is this used?
================
Comment at: libcxx/include/__algorithm/ranges_find_last.h:16
+#include <__functional/invoke.h>
+#include <__functional/ranges_operations.h>
+#include <__iterator/concepts.h>
----------------
Is this used?
================
Comment at: libcxx/include/__algorithm/ranges_find_last.h:37-38
+ template <forward_iterator _Ip, sentinel_for<_Ip> _Sp, class _Tp, class _Proj = identity>
+ requires indirect_binary_predicate < ranges::equal_to, projected<_Ip, _Proj>,
+ const _Tp* > _LIBCPP_HIDE_FROM_ABI constexpr subrange<_Ip>
+ operator()(_Ip __first, _Sp __last, const _Tp& __value, _Proj __proj = {}) const {
----------------
This formatting looks really weird, is it produced by `clang-format`? (If so, feel free to disable it for these lines by adding a `// clang-format off` comment, then reenable with `// clang-format on`)
================
Comment at: libcxx/include/__algorithm/ranges_find_last.h:38
+ requires indirect_binary_predicate < ranges::equal_to, projected<_Ip, _Proj>,
+ const _Tp* > _LIBCPP_HIDE_FROM_ABI constexpr subrange<_Ip>
+ operator()(_Ip __first, _Sp __last, const _Tp& __value, _Proj __proj = {}) const {
----------------
Can you also add `_LIBCPP_NODISCARD_EXT` to all the new algorithms? We want to mark these as `[[nodiscard]]` since they have no side effects and are only used to produce a value, but since they aren't marked as such in the Standard, we have to do it as an extension. Please also update the `test/libcxx/diagnostics/ranges.nodiscard_extensions.verify.cpp` test file.
================
Comment at: libcxx/include/__algorithm/ranges_find_last.h:40
+ operator()(_Ip __first, _Sp __last, const _Tp& __value, _Proj __proj = {}) const {
+ auto __pred = [&](auto&& __e) { return std::forward<decltype(__e)>(__e) == __value; };
+ return ranges::__find_last_if_impl(std::move(__first), std::move(__last), __pred, __proj);
----------------
Question: what's the case where `forward` would make a difference? Do we have a test for that?
================
Comment at: libcxx/include/__algorithm/ranges_find_last.h:61
+
+#endif // _LIBCPP_STD_VER > 23
+
----------------
`>= 23`.
================
Comment at: libcxx/include/__algorithm/ranges_find_last_if.h:38
+__find_last_if_impl(_Ip __first, _Sp __last, _Pred __pred, _Proj __proj) {
+ _Ip __current = __first;
+ subrange<_Ip> __result{__last, __last};
----------------
Normally we simply increment `__first`, no need to create a separate variable.
================
Comment at: libcxx/include/__algorithm/ranges_find_last_if.h:38-46
+ _Ip __current = __first;
+ subrange<_Ip> __result{__last, __last};
+ while (__current != __last) {
+ if (std::invoke(__pred, std::invoke(__proj, *__current))) {
+ __result = {__current, __last};
+ }
+ ++__current;
----------------
var-const wrote:
> Normally we simply increment `__first`, no need to create a separate variable.
Recreating the whole `__result` each time seems a little wasteful, consider:
```
_Ip __result = __last;
while (__first != __last) {
if (std::invoke(__pred, std::invoke(__proj, * __first))) {
__result = __first;
}
++__first;
}
return {__result, __last};
```
================
Comment at: libcxx/include/__algorithm/ranges_find_last_if.h:44
+ }
+ ++__current;
+ }
----------------
For a bidirectional common range, it seems more efficient to iterate from the end -- then you can return as soon as you find an element satisfying the predicate. For a bidirectional non-common range, it might still be more efficient to create an iterator from the sentinel and go from the end (though this is debatable).
================
Comment at: libcxx/include/__algorithm/ranges_find_last_if.h:54
+ indirect_unary_predicate<projected<iterator_t<_Rp>, _Proj>> _Pred>
+ _LIBCPP_NODISCARD_EXT constexpr borrowed_subrange_t<_Rp>
+ operator()(_Rp&& __r, _Pred __pred, _Proj __proj = {}) const {
----------------
Missing `_LIBCPP_HIDE_FROM_ABI`.
================
Comment at: libcxx/include/__algorithm/ranges_find_last_if.h:57
+ return ranges::__find_last_if_impl(ranges::begin(__r), ranges::end(__r), __pred, __proj);
+ }
+ template <forward_iterator _Ip,
----------------
Please add a newline after this line.
================
Comment at: libcxx/include/__algorithm/ranges_find_last_if.h:62
+ indirect_unary_predicate<projected<_Ip, _Proj>> _Pred>
+ _LIBCPP_NODISCARD_EXT constexpr subrange<_Ip>
+ operator()(_Ip __first, _Sp __last, _Pred __pred, _Proj __proj = {}) const {
----------------
Missing `_LIBCPP_HIDE_FROM_ABI`.
================
Comment at: libcxx/include/__algorithm/ranges_find_last_if.h:76
+
+#endif // _LIBCPP_STD_VER > 23
+
----------------
`>= 23`.
================
Comment at: libcxx/include/__algorithm/ranges_find_last_if_not.h:15
+#include <__functional/identity.h>
+#include <__functional/invoke.h>
+#include <__functional/ranges_operations.h>
----------------
`iterator_operations.h` might be unnecessary.
================
Comment at: libcxx/include/__algorithm/ranges_find_last_if_not.h:42
+ operator()(_Ip __first, _Sp __last, _Pred __pred, _Proj __proj = {}) const {
+ auto __not_pred = [&](auto&& __e) { return !std::invoke(__pred, std::forward<decltype(__e)>(__e)); };
+ return ranges::__find_last_if_impl(std::move(__first), std::move(__last), __not_pred, __proj);
----------------
Do we have a test that fails if we remove this `forward`?
================
Comment at: libcxx/include/__algorithm/ranges_find_last_if_not.h:51
+ operator()(_Rp&& __r, _Pred __pred, _Proj __proj = {}) const {
+ auto __not_pred = [&](auto&& __e) { return !std::invoke(__pred, std::forward<decltype(__e)>(__e)); };
+ return ranges::__find_last_if_impl(ranges::begin(__r), ranges::end(__r), __not_pred, __proj);
----------------
To avoid duplicating this logic, consider instead delegating to the iterator version of `find_last_if_not`.
================
Comment at: libcxx/include/__algorithm/ranges_find_last_if_not.h:64
+
+#endif // _LIBCPP_STD_VER > 23
+
----------------
`>= 23`
================
Comment at: libcxx/include/__algorithm/ranges_find_last_if_not.h:42
+ operator()(_Ip __first, _Sp __last, _Pred __pred, _Proj __proj = {}) const {
+ auto __pred2 = [&](auto&& __e) { return !std::invoke(__pred, std::forward<decltype(__e)>(__e)); };
+ return ranges::__find_last_if_impl(std::move(__first), std::move(__last), __pred2, __proj);
----------------
Mordante wrote:
> naming is hard, but let's try to use something better than just a number.
> How about `__not_pred` since this is basically the inverse of the predicate given.
>
> Another option is just not giving this predicate a name and write it in the return statement on the next line.
Very optional: `__negate_pred` or `__negated_pred`?
================
Comment at: libcxx/include/algorithm:102
+ template<forward_iterator I, sentinel_for<I> S, class T, class Proj = identity>
+ requires indirect_binary_predicate<ranges::equal_to, projected<I, Proj>, const T*>
----------------
Nit: please fix indentation here.
================
Comment at: libcxx/include/algorithm:104
+ requires indirect_binary_predicate<ranges::equal_to, projected<I, Proj>, const T*>
+ constexpr subrange<I> ranges::find_last(I first, S last, const T& value, Proj proj = {}); // since C++23
+
----------------
Nit: can these comments (`// since C++23`) be aligned?
================
Comment at: libcxx/test/std/algorithms/alg.nonmodifying/alg.find/ranges.find_last.pass.cpp:145
+ {
+ // check that std::invoke is used
+ struct S {
----------------
This test should also apply to the iterator version of the algorithm, right? (applies to the other two test files as well)
================
Comment at: libcxx/test/std/algorithms/alg.nonmodifying/alg.find/ranges.find_last.pass.cpp:164
+ {
+ int a[] = {1, 2, 2, 3, 4};
+ int projection_count = 0;
----------------
Wrong indentation.
================
Comment at: libcxx/test/std/algorithms/alg.nonmodifying/alg.find/ranges.find_last.pass.cpp:175
+ {
+ OneWayComparable a[] = {OneWayComparable{true}};
+ auto ret = std::ranges::find_last(a, a + 1, OneWayComparable{false});
----------------
Do the other two files need an equivalent test?
================
Comment at: libcxx/test/std/algorithms/alg.nonmodifying/alg.find/ranges.find_last.pass.cpp:215
+ std::array<int, 0> a = {};
+ constexpr int search_value = 1;
+ auto ret = std::ranges::find_last(a.begin(), a.end(), search_value);
----------------
Does this have to be `constexpr`?
================
Comment at: libcxx/test/std/algorithms/alg.nonmodifying/alg.find/ranges.find_last.pass.cpp:230
+ {
+ StrictComparable<int> a[] = {1, 2, 2, 3, 4};
+ auto ret = std::ranges::find_last(a, a + 4, StrictComparable<int>{2});
----------------
The other files use `BooleanComparable`, can this be made consistent?
================
Comment at: libcxx/test/std/algorithms/alg.nonmodifying/alg.find/ranges.find_last.pass.cpp:81
+ int a[] = {1, 2, 3, 4};
+ auto ret = std::ranges::find_last(a, a + 4, a + 3, [](int& i) { return &i; });
+ assert(ret.data() == a + 3);
----------------
Mordante wrote:
> Here you should use `std::same_as<return type> auto` method. For example have a look at
> `libcxx/test/std/ranges/range.adaptors/range.common.view/begin.pass.cpp`
Looks not done?
================
Comment at: libcxx/test/std/algorithms/alg.nonmodifying/alg.find/ranges.find_last_if.pass.cpp:9
+
+// <algorithm>
+
----------------
Note: most comments in this file apply to the other two test files as well.
================
Comment at: libcxx/test/std/algorithms/alg.nonmodifying/alg.find/ranges.find_last_if.pass.cpp:35
+template <class It, class Sent = It>
+concept HasFindIfIt = requires(It it, Sent sent) {
+ std::ranges::find_last_if(it, sent, Predicate{});
----------------
`s/HasFindIfIt/HasFindLastIfIt/`, same for the range version below.
================
Comment at: libcxx/test/std/algorithms/alg.nonmodifying/alg.find/ranges.find_last_if.pass.cpp:53
+
+static_assert(!HasFindIfPred<IndirectUnaryPredicateNotCopyConstructible>);
+static_assert(!HasFindIfPred<IndirectUnaryPredicateNotPredicate>);
----------------
Please also add a case where `HasFindLastIfPred` is `true` (to test the test, so to speak).
================
Comment at: libcxx/test/std/algorithms/alg.nonmodifying/alg.find/ranges.find_last_if.pass.cpp:71
+ {
+ int a[] = {1, 2, 3, 4};
+ std::same_as<std::ranges::subrange<It>> auto ret =
----------------
Can we add a few more test cases?
- empty range;
- 1-element range;
- no element satisfies the predicate;
- all elements satisfy the predicate;
- the element being searched is the first one;
- the element being searched is the last one.
================
Comment at: libcxx/test/std/algorithms/alg.nonmodifying/alg.find/ranges.find_last_if.pass.cpp:73
+ std::same_as<std::ranges::subrange<It>> auto ret =
+ std::ranges::find_last_if(It(a), Sent(It(a + 4)), [](int x) mutable { return x == 4; });
+ assert(base(ret.begin()) == a + 3);
----------------
This formatting is incorrect, continuations should be indented.
================
Comment at: libcxx/test/std/algorithms/alg.nonmodifying/alg.find/ranges.find_last_if.pass.cpp:73
+ std::same_as<std::ranges::subrange<It>> auto ret =
+ std::ranges::find_last_if(It(a), Sent(It(a + 4)), [](int x) mutable { return x == 4; });
+ assert(base(ret.begin()) == a + 3);
----------------
var-const wrote:
> This formatting is incorrect, continuations should be indented.
Why is it `mutable`?
================
Comment at: libcxx/test/std/algorithms/alg.nonmodifying/alg.find/ranges.find_last_if.pass.cpp:78
+ {
+ int a[] = {1, 2, 3, 4};
+ auto range = std::ranges::subrange(It(a), Sent(It(a + 4)));
----------------
Please reuse the input between the iterator version and the range version, it cuts down boilerplate and makes it easier to verify that the two test cases are equivalent.
================
Comment at: libcxx/test/std/algorithms/alg.nonmodifying/alg.find/ranges.find_last_if.pass.cpp:87
+
+struct NonConstComparable {
+ friend constexpr bool operator==(const NonConstComparable&, const NonConstComparable&) { return false; }
----------------
It's better if the tests for these very similar algorithms are as uniform as possible. The `find_last_if_not` test file calls this helper `NonConstComparableLvalue` while the `find_last` test file has both `NonConstComparableLValue ` and `NonConstComparableRValue `. Can this be consistent?
================
Comment at: libcxx/test/std/algorithms/alg.nonmodifying/alg.find/ranges.find_last_if.pass.cpp:88
+struct NonConstComparable {
+ friend constexpr bool operator==(const NonConstComparable&, const NonConstComparable&) { return false; }
+ friend constexpr bool operator==(NonConstComparable&, NonConstComparable&) { return false; }
----------------
Are all 4 overloads necessary? If so, please add a short comment to explain why.
================
Comment at: libcxx/test/std/algorithms/alg.nonmodifying/alg.find/ranges.find_last_if.pass.cpp:95
+constexpr bool test() {
+ test_iterators<int*>();
+ test_iterators<const int*>();
----------------
Consider using `types::for_each` to cut down on the boilerplate a little.
================
Comment at: libcxx/test/std/algorithms/alg.nonmodifying/alg.find/ranges.find_last_if.pass.cpp:97
+ test_iterators<const int*>();
+ test_iterators<bidirectional_iterator<int*>>();
+ test_iterators<forward_iterator<int*>>();
----------------
Can these types be ordered from most to least restrictive? (or vice versa)
================
Comment at: libcxx/test/std/algorithms/alg.nonmodifying/alg.find/ranges.find_last_if.pass.cpp:102
+
+ { // check that projections are used properly and that they are called with the iterator directly
+ {
----------------
Can you rephrase the `called with the iterator directly` bit? It reads as if the projection receives an iterator as an argument, which isn't the case. I think something like `called with the reference to the element the iterator is pointing to` would be more technically correct (though this can definitely be improved).
================
Comment at: libcxx/test/std/algorithms/alg.nonmodifying/alg.find/ranges.find_last_if.pass.cpp:106
+ auto ret = std::ranges::find_last_if(
+ a, a + 4, [&](int* i) { return i == a + 3; }, [](int& i) { return &i; });
+ assert(ret.data() == a + 3);
----------------
Please make the projection a named variable and reuse it between the iterator version and the range version.
================
Comment at: libcxx/test/std/algorithms/alg.nonmodifying/alg.find/ranges.find_last_if.pass.cpp:107-108
+ a, a + 4, [&](int* i) { return i == a + 3; }, [](int& i) { return &i; });
+ assert(ret.data() == a + 3);
+ }
+ {
----------------
Optional: consider adding blank lines between scopes (throughout). I find code easier to read when it's split into logical blocks, and different scopes offer a very straightforward point for splitting.
================
Comment at: libcxx/test/std/algorithms/alg.nonmodifying/alg.find/ranges.find_last_if.pass.cpp:121
+ int comp;
+ int other;
+ };
----------------
How about `s/other/index/`?
================
Comment at: libcxx/test/std/algorithms/alg.nonmodifying/alg.find/ranges.find_last_if.pass.cpp:130
+ {
+ struct S {
+ int comp;
----------------
(throughout) You can reuse these helper types between the iterator version and the range version to reduce boilerplate.
================
Comment at: libcxx/test/std/algorithms/alg.nonmodifying/alg.find/ranges.find_last_if.pass.cpp:143
+
+ { // check that end + 1 iterator is returned with no match
+ {
----------------
Nit: `s/end + 1/past-the-end/`.
================
Comment at: libcxx/test/std/algorithms/alg.nonmodifying/alg.find/ranges.find_last_if.pass.cpp:143
+
+ { // check that end + 1 iterator is returned with no match
+ {
----------------
var-const wrote:
> Nit: `s/end + 1/past-the-end/`.
I would move this test to `test_iterators`.
================
Comment at: libcxx/test/std/algorithms/alg.nonmodifying/alg.find/ranges.find_last_if.pass.cpp:158
+ // check that ranges::dangling is returned
+ [[maybe_unused]] std::same_as<std::ranges::dangling> auto ret =
+ std::ranges::find_last_if(std::array{1, 2}, [](int) { return false; });
----------------
Nit: I'd use `decltype(auto)`. In this case the difference isn't really relevant, but IMO it's better to just always use `decltype(auto)` which catches the exact return type and never have to think about it.
================
Comment at: libcxx/test/std/algorithms/alg.nonmodifying/alg.find/ranges.find_last_if.pass.cpp:182
+ int a[] = {1, 2, 2, 3, 4};
+ int predicate_count = 0;
+ int projection_count = 0;
----------------
Please see if the types from `test/support/counting_{projection,predicates}.h` can be used here.
================
Comment at: libcxx/test/std/algorithms/alg.nonmodifying/alg.find/ranges.find_last_if.pass.cpp:208
+
+ { // check that the return type of `iter::operator*` doesn't change
+ {
----------------
Can you please elaborate a little on this comment? I'm not sure how it relates to the test (which seems to check whether a predicate that takes arguments by a non-const reference works with the algorithm).
================
Comment at: libcxx/test/std/algorithms/alg.nonmodifying/alg.find/ranges.find_last_if.pass.cpp:221
+
+ { // check that an empty range works
+ {
----------------
Please move this test case to `test_iterators`.
================
Comment at: libcxx/test/std/algorithms/alg.nonmodifying/alg.find/ranges.find_last_if_not.pass.cpp:73
+ std::same_as<std::ranges::subrange<It>> auto ret =
+ std::ranges::find_last_if_not(It(a), Sent(It(a + 5)), [c = 0](int) mutable { return c++ <= 2; });
+ assert(base(ret.begin()) == a + 4);
----------------
Can we use a simpler predicate? These tests should check the most basic logic and be as easy to read as possible.
================
Comment at: libcxx/test/std/algorithms/alg.nonmodifying/alg.find/ranges.find_last_if_not.pass.cpp:118
+ {
+ // check that the first element is returned
+ {
----------------
Last element?
================
Comment at: libcxx/test/std/algorithms/alg.nonmodifying/alg.find/ranges.find_last_if_not.pass.cpp:254
+
+ return 0;
+}
----------------
Please also update `test/std/library/description/conventions/customization.point.object/niebloid.compile.pass.cpp` and the `*robust*` test files where applicable (searching for `ranges::find_if` among the `*robust*` test files should give a good idea of which ones to update).
Repository:
rG LLVM Github Monorepo
CHANGES SINCE LAST ACTION
https://reviews.llvm.org/D136522/new/
https://reviews.llvm.org/D136522
More information about the libcxx-commits
mailing list