Details
- Reviewers
• Quuxplusone var-const Mordante - Group Reviewers
Restricted Project - Commits
- rGf83d833e41d7: [libc++][ranges] Implement ranges::min
Diff Detail
- Repository
- rG LLVM Github Monorepo
Event Timeline
The headline here is that we need to remember to std::invoke our predicates and projections. Sorry I'm just catching this now.
libcxx/include/__algorithm/ranges_min.h | ||
---|---|---|
17 | Granularize? | |
35 | Argh! Peeking at range-v3's code has just reminded me that we need to be using std::invoke for projections and comparators. Please fix here and probably in some earlier reviews too (sadly). struct S { int i; }; S a[3] = { S{2}, S{1}, S{3} }; decltype(auto) ret = std::ranges::min(a[0], a[1], {}, &S::i); ASSERT_SAME_TYPE(decltype(ret), const S&); assert(&ret == &a[1]); assert(ret.i == 1); and likewise for the initializer_list and range overloads. | |
42 | This works, but why not just return *ranges::min_element(ranges::begin(__r), ranges::end(__r)...... oh, because you don't want to copy the comparator or the projection, right! AFAICT, range-v3 takes the middle path: it doesn't unnecessarily copy callables (much?), but it also takes no special precautions to avoid move-constructing them. We could go that route, but I think avoiding unnecessary moves is a QoI thing and we might as well do it right the first time. | |
libcxx/test/std/algorithms/alg.sorting/alg.min.max/ranges.min.pass.cpp | ||
44–45 | You probably intended static_assert( HasMin<int(&)[10]>); static_assert(!HasMin<NoLessThanOp(&)[10]>); static_assert(!HasMin<NotTotallyOrdered(&)[10]>); and/or static_assert(!HasMin<std::initializer_list<NoLessThanOp>>); static_assert(!HasMin<std::initializer_list<NotTotallyOrdered>>); FYI, I'm getting more and more uncomfortable with this HasXxx pattern, because it makes it impossible to test rvalue types. I think you should switch, at least in new code, to static_assert(std::invocable<decltype(std::ranges::min), std::initializer_list<int>>); static_assert(std::invocable<decltype(std::ranges::min), int(&)[10]>); static_assert(std::invocable<decltype(std::ranges::min), int(&&)[10]>); // impossible to express right now | |
53 | I'd like a test for decltype(ranges::min(1,2)), etc. — it should return a const int&. | |
76 | This should be std::same_as<int> decltype(auto) if that's allowed (I think it probably isn't), or else write it out longhand with decltype(auto) ret = ~~~ ASSERT_SAME_TYPE(decltype(ret), ~~~). What we're worried about here is that this overload of ranges::min should return exactly int, not a const int& that might dangle. | |
85 | Nit: consider auto projection = []... to shorten this line. | |
103 | Remember that [[maybe_unused]] is a code smell! | |
107 | views::all isn't just borrowed, but also a view; and I don't think either of those properties are "interesting" to ranges::min, are they? This test case might be redundant. |
Please git grep ranges::min and fix what you find. std/library/description/conventions/customization.point.object/niebloid.compile.pass.cpp needs updating, at least.
- Address comments
libcxx/test/std/algorithms/alg.sorting/alg.min.max/ranges.min.pass.cpp | ||
---|---|---|
44–45 | I copy/pasted that part, so it's probably also not tested properly in the other test.
concept HasMin = requires(T&& t) { std::ranges::min(std::forward<T>(t)); } should work for these cases, right? | |
76 | At least clang is happy with concept decltype(auto). It also works as expected. |
libcxx/test/std/algorithms/alg.sorting/alg.min.max/ranges.min.pass.cpp | ||
---|---|---|
44–45 | Yes, it should; or even concept HasMin = requires { std::ranges::min(std::declval<T>()); }. I'm ambivalent as to the readability of that versus a straight std::invocable<RangeMinT, ...>, so, either is fine AFAIC. | |
76 |
Yep, Godbolt says everyone's happy with it, so, OK. |
libcxx/test/std/algorithms/alg.sorting/alg.min.max/ranges.min.pass.cpp | ||
---|---|---|
80 | __iterator/projected.h:28:34: error: 'std::__1::indirect_result_t<_Proj&, _It> std::__1::projected<_It, _Proj>::operator*() const [with _It = const test_initializer_list()::<lambda(int, int)>*; _Proj = std::__1::identity; std::__1::indirect_result_t<_Proj&, _It> = const test_initializer_list()::<lambda(int, int)>&]', declared using local type 'const test_initializer_list()::<lambda(int, int)>', is used but never defined [-fpermissive] 28 | indirect_result_t<_Proj&, _It> operator*() const; // not defined | ^~~~~~~~ Kind of, but I think it's a GCC bug. GCC is complaining about the use of the "local type" decltype(comparator). We're instantiating projected<int*, decltype(comparator)> and declaring its operator*, but there's no way that operator* can ever be defined outside of this TU, and it's also not defined inside this TU, so that's a problem... at least for GCC, for some reason. (That is, I think GCC is mistakenly considering operator* to have been ODR-used, even though we're only using it in non-evaluated contexts AFAICT.) |
libcxx/test/std/algorithms/alg.sorting/alg.min.max/ranges.min.pass.cpp | ||
---|---|---|
80 | Looks like a similar problem to https://gcc.gnu.org/PR92894 (and its https://gcc.gnu.org/PR94241 dup) which we decided was a problem with using a deduced return type in the library code, not a compiler bug. |
@philnik Can you also update RangesAlgorithms.csv?
libcxx/include/__algorithm/ranges_min.h | ||
---|---|---|
62 | Question: the Standard has the requirement that the range is not empty for both this overload and the initializer_list overload, but it is only checked here. AFAICT, __min_element::__fn::__go doesn't do this check. Is there a reason to only apply this check here? | |
67 | Some thoughts: this could lead to a lot of copying in the worst case. min_element doesn't have this problem because it stores the iterator instead; however, it's fine for min_element operating on forward_iterators but cannot be used here since we're iterating an input_range. It probably doesn't matter in most scenarios, but in the worst case, I could imagine that calling min on std::vector<ExpensiveToCopy> could result in copying every single element if the vector happens to be in descending order. I wonder if it would be worthwhile to have an if constexpr to only copy iterators if the range is a forward range, and fall back to this implementation otherwise? | |
libcxx/include/__ranges/take_view.h | ||
121 | Thanks for going back to fix this! Sorry about the annoying comment -- do you know if this change affects the visible behavior of take_view (e.g., some code compiles now that wouldn't compile previously, or vice versa)? If yes, could you please add a test(s) for this? | |
libcxx/include/algorithm | ||
54 | Does the overload taking an initializer_list also need to be added? | |
libcxx/test/std/algorithms/alg.sorting/alg.min.max/ranges.min.pass.cpp | ||
22 | The initializer_list overload also seems to be missing from here. | |
27 | Nit: <random> seems unused? | |
44 | Should this be TotallyOrderedRval? Also, wouldn't this class need at least an equality operator to be totally ordered? | |
70 | Nit: it seems like this test uses both static_assert and ASSERT_SAME_TYPE for comparing types, can it be consistent? | |
123 | Nit: consider adding newlines between braced scopes. I just think it makes it easier to separate different test cases visually. | |
161 | It seems like more SFINAE tests are needed -- I don't think constraints such as input_range, copyable, indirectly_copyable_storable, etc. are currently being checked. |
- Address comments
libcxx/include/__algorithm/ranges_min.h | ||
---|---|---|
67 | I thought about that too, let's see what @Quuxplusone thinks. | |
libcxx/include/__ranges/take_view.h | ||
121 | I don't think so, but I could be wrong. |
libcxx/include/__algorithm/ranges_min.h | ||
---|---|---|
62 | Nice catch! Wording nit: s/has to/must/ It is difficult, but possible, to create an empty initializer_list: std::initializer_list<int> il = {}; std::ranges::min(il); // UB I agree let's be consistent: either both should _LIBCPP_ASSERT or neither should. I have no particular preference which. @var-const, both ranges::min_element and std::min_element are totally fine with a zero-element range; in that case there is no min element found, so they return last (which points to no element of the range). | |
67 | Good point! I'm slightly worried there might be a subtlety we're all missing, but let's run with it for now. | |
libcxx/include/__ranges/take_view.h | ||
121 | This is just taking the min of two integers both of type range_size_t<_View>. I don't think there's any possible way for the behavior to be different. using _SizeT = range_size_t<_View>; _SizeT __n = ranges::size(__base_); return __n < static_cast<_SizeT>(__count_) ? __n : static_cast<_SizeT>(__count_); and not pay for the instantiation (or inclusion) of ranges::min at all. (But generally libc++ style is to follow the reference implementation to the letter, so as to avoid bugs due to exactly the kind of over-cleverness I'm displaying here. So I recommend keeping this as ranges::min.) | |
libcxx/test/std/algorithms/alg.sorting/alg.min.max/ranges.min.pass.cpp | ||
123 | Ultranit: I have a slight preference for not newlines between braced scopes — two almost-empty lines is enough separation for me. However, I also would change line 88 from { // test comparator to { // test comparator (and likewise line 82, 129, and anywhere else I missed). Please make the line 82/88/etc change; and I recommend re-removing the blank lines, but won't die on that hill. :) |
- Address comments
libcxx/include/__algorithm/ranges_min.h | ||
---|---|---|
67 | According to https://godbolt.org/z/fjaxc9rT5 it doesn't get instantiated. |
libcxx/include/algorithm | ||
---|---|---|
48 | Hmm, looking at the Standard, I see template<class T, class Proj = identity, indirect_strict_weak_order<projected<const T*, Proj>> Comp = ranges::less> constexpr const T& ranges::min(const T& a, const T& b, Comp comp = {}, Proj proj = {}); // initializer_list overload looks the same template<input_range R, class Proj = identity, indirect_strict_weak_order<projected<iterator_t<R>, Proj>> Comp = ranges::less> requires indirectly_copyable_storable<iterator_t<R>, range_value_t<R>*> constexpr range_value_t<R> ranges::min(R&& r, Comp comp = {}, Proj proj = {}); Should this be fixed (here and in the test file)? | |
libcxx/test/std/algorithms/alg.sorting/alg.min.max/ranges.min.pass.cpp | ||
22 | Nit: for consistency, please either add a newline between these two overloads, or remove the newline above (I'd prefer the former). | |
161 | Hmm, I still don't see a check for input_range and indirectly_copyable_storable. |
libcxx/include/algorithm | ||
---|---|---|
48 | Yikes, good catch! (The current PR has constexpr I min(I first, S last, ... presumably copy-pasted from min_element.) It'll also be a very good idea to make sure we have at least one test for, like, std::ranges::min(p, p+1) (takes the min of the pointer values, not the min element of the range), and also a SFINAE test for std::ranges::min(It, Sent) where the It and Sent are of different types. If these tests already exist, awesome. |
libcxx/include/__algorithm/ranges_min.h | ||
---|---|---|
50–51 | (1) I was going to comment "Is it safe to call ranges::begin twice on an arbitrary range?" (I think not). But then noticed that this is just an initializer_list. Please s/__r/__il/g as a matter of naming. (2) Depending on how https://reviews.llvm.org/D121248#inline-1160030 is resolved, this may need to become ranges::__min_element_impl(...). @ldionne, PTAL at my argument over there; WDYT? |
The code looks good, but it seems we lack a bit of test coverage.
libcxx/test/std/algorithms/alg.sorting/alg.min.max/ranges.min.pass.cpp | ||
---|---|---|
93 | I miss tests to compare the Complexity Complexity: Exactly one comparison and two applications of the projection, if any. | |
94 | I miss a test for two equivalent values. Returns: The smaller value. Returns the first argument when the arguments are equivalent. | |
148 | Please add a test with multiple equivalent values:
That way we make sure we validate Returns: The smallest value in the input range. Returns a copy of the leftmost element when several elements are equivalent to the smallest. The same for test_range | |
151 | I like a separate test like this one, but using an input_iterator. It seems that code path is completely untested. |
Granularize?