The first view in the libc++ ranges library 🚀
Details
- Reviewers
cjdb ldionne EricWF • Quuxplusone - Group Reviewers
Restricted Project - Commits
- rG560170fa2de5: [libcxx][views] Add drop_view.
Diff Detail
- Repository
- rG LLVM Github Monorepo
Event Timeline
Apply some of the comments.
I still need to:
-> Add a test for move-only input-iterator.
-> Expand the const tests.
-> Add tests for all_t CTAD.
libcxx/include/__ranges/drop_view.h | ||
---|---|---|
57 | I'd put __count before __base - see https://lists.isocpp.org/lib/2020/09/17540.php. |
libcxx/include/__ranges/drop_view.h | ||
---|---|---|
45 | Yep. This is the original use case of non-propagating-cache in range-v3. |
libcxx/include/__ranges/drop_view.h | ||
---|---|---|
45 | The standard is very underspecified in what kind of cache is necessary here. I just checked, and range-v3 and cmcstl2 disagree on what kind of cache is necessary: range-v3 uses a non-propagating cache; cmcstl2 switches between a non-propagating cache and a simple cache, depending on whether or not we have a forwarding-range (which I think is now borrowed_range). |
libcxx/include/__ranges/drop_view.h | ||
---|---|---|
45 |
It falls out of the fact that nothing in the standard allows "take a drop_view, call begin() on it, copy it, and call begin() on the copy" to not work. |
- Some of my comments (especially about splitting tests) also apply to other in-flight reviews like transform_view.
- Please don't forget to mark comments as done when they're not relevant anymore (whether they have been implemented or you've decided you wouldn't do it), so it's easy to keep track of things.
I'll wait for this to be rebased onto non_propagating_cache before I re-review the implementation.
libcxx/test/std/ranges/range.adaptors/range.drop/range.drop.view.pass.cpp | ||
---|---|---|
1 ↗ | (On Diff #344559) | Please split this into more files as it makes sense. Each file tests one thing, and what's being tested is at the top of level in a comment, as we always do. |
23 ↗ | (On Diff #343544) | I'll actually disagree here - I think it's good to stay minimal when we don't lose anything doing so. |
There was quite a bit of discussion offline about the need to invalidate the cached begin() or not. I think we now all agree that we must not propagate the cached value on copies or moves. Here's what I ended up with that is not dependent on non_propagating_cache (we can go back afterwards and perhaps simplify some of this if we have non_propagating_cache):
namespace ranges { template<class _Range> concept __simple_view = view<_Range> && range<const _Range> && same_as<sentinel_t<_Range>, iterator_t<const _Range>> && same_as<iterator_t<_Range>, sentinel_t<const _Range>>; template<view _View> class drop_view : public view_interface<drop_view<_View>>, { static constexpr _UseCache = forward_range<_View> && !random_access_range<_View>; using _Cache = optional<iterator_t<_View>>; struct _Empty { }; [[no_unique_address]] conditional_t<_UseCache, _Cache, _Empty> __cached_begin_; _View __base_; range_difference_t<_View> __count_; public: constexpr drop_view() requires default_initializable<_View> : __cached_begin_(), __base_(), __count_(0) { } constexpr drop_view(_View __base, range_difference_t<_View> __count) : __cached_begin_(), __base_(_VSTD::move(__base)), __count_(__count) { _LIBCPP_ASSERT(__count_ >= 0, "count must be greater than or equal to zero."); } constexpr drop_view(drop_view const& __other) : __cached_begin_() // purposefully not propagating the cached value , __base_(__other.__base_) , __count_(__other.__count_) { } constexpr drop_view(drop_view&& __other) : __cached_begin_() // purposefully not propagating the cached value , __base_(_VSTD::move(__other.__base_)) , __count_(_VSTD::move(other.__count_)) { } constexpr drop_view& operator=(drop_view const& __other) { if constexpr (_UseCache) { __cached_begin_.reset(); } __base_ = __other.__base_; __count_ = __other.__count_; return *this; } constexpr drop_view& operator=(drop_view&& __other) { if constexpr (_UseCache) { __cached_begin_.reset(); __other.__cached_begin_.reset(); } __base_ = _VSTD::move(__other.__base_); __count_ = _VSTD::move(__other.__count_); return *this; } constexpr _View base() const& requires copy_constructible<_View> { return __base_; } constexpr _View base() && { return _VSTD::move(__base_); } constexpr auto begin() requires (!(__simple_view<_View> && random_access_range<const _View> && sized_range<const _View>)) { if constexpr (_UseCache) { if (__cached_begin_.empty()) __cached_begin_ = ranges::next(ranges::begin(__base_), __count_, ranges::end(__base_)); return *__cached_begin_; } else { return ranges::next(ranges::begin(__base_), __count_, ranges::end(__base_)); } } constexpr auto begin() const requires random_access_range<const _View> && sized_range<const _View> { return ranges::next(ranges::begin(__base_), __count_, ranges::end(__base_)); } constexpr auto end() requires (!__simple_view<_View>) { return ranges::end(__base_); } constexpr auto end() const requires range<const _View> { return ranges::end(__base_); } static constexpr auto __size(auto& __self) { const auto __s = ranges::size(__self.__base_); const auto __c = static_cast<decltype(__s)>(__self.__count_); return __s < __c ? 0 : __s - __c; } constexpr auto size() requires sized_range<_View> { return drop_view::__size(*this); } constexpr auto size() const requires sized_range<const _View> { return drop_view::__size(*this); } }; template<class _Range> drop_view(_Range&&, range_difference_t<_Range>) -> drop_view<views::all_t<_Range>>; } // namespace ranges
libcxx/include/__ranges/drop_view.h | ||
---|---|---|
36 | This was already done. | |
42 | I'm going to leave it at the top. I don't really see a reason to put it at the bottom, especially when classes have private members by default. | |
45 | For posterity: marking this whole chain as resolved after our discussion on discord. | |
libcxx/test/std/ranges/range.adaptors/range.drop/range.drop.view.pass.cpp | ||
20 ↗ | (On Diff #344559) | Remove this. |
libcxx/include/__ranges/drop_view.h | ||
---|---|---|
42 |
Please do as @ldionne says, and follow libc++ style. Private/data members go at the bottom (and data members get a suffix-underscore; thanks for addressing that part already). |
@ldionne wrote:
Here's what I ended up with that is not dependent on non_propagating_cache (we can go back afterwards and perhaps simplify some of this if we have non_propagating_cache)
FYI, that looks pretty nice to me. And then non_propagating_cache would just be a thin wrapper around an optional — all it needs to provide is .has_value(), .get() (maybe more than one ref-qualification of .get()?), .assign (i.e. the-equivalent-of-operator=, not the-equivalent-of-.emplace), and a default constructor. One might reasonably say "If Louis' code works, then why do we even want a factored-out non_propagating_cache type?" But, I answer, it's still useful to wrap optional, in order to reduce its surface area to just these four necessary bits.
Per offline discussion, I'm okay with this going ahead, provided we return to add __non_propagating_cache once I've landed it.
libcxx/include/__ranges/drop_view.h | ||
---|---|---|
134 | Please line up requires-clauses with the start of the function declaration. Small clause (such as this) can go on the same line. I've usually turned a blind eye to this because comments about formatting are noise that lead to unproductive debates, and my intention has been to get clang-format to take care of this inconsistency in one fell swoop. Having said that, since I don't know when I'll be able to apply the tool project-wide and not get something weird, it's probably worth adopting the "official" libc++ style for future contributors' sakes. | |
142–145 | Does this need to be fixed? | |
libcxx/test/std/ranges/range.adaptors/range.drop/general.pass.cpp | ||
62 | I like that you're using this! |
libcxx/include/__ranges/drop_view.h | ||
---|---|---|
38 | The cache needs to be used for random access ranges too unless the range is also sized. |
libcxx/include/__ranges/drop_view.h | ||
---|---|---|
134 | FWIW, I agree about "unproductive debates" ;) but if there's an official libc++ style, it's always been "indent the subordinate clause by one level." template<class T> requires foo<T> auto bar() -> decltype(baz) { return quux; } I don't think your insistence on following clang-format's (broken) formatting guidance here is helpful to the project. I predict that clang-format will eventually implement C++20 formatting, and then it'll be good that Zoe's been following the "indent requires-clauses" style. |
libcxx/include/__ranges/drop_view.h | ||
---|---|---|
134 | Louis gave me creative freedom to set the style for requires-clauses. I made the decision that they wouldn't be indented. |
I would like to see tests for the constructors. They're going to be simple tests, but we should be testing everything systematically. Elsewhere we have xxxx.ctor.pass.cpp tests. Also, can you please explicitly mark where you're testing each overload of begin() and end()?
I've got a few comments, but they are pretty small. We should be able to land this today.
libcxx/include/__ranges/drop_view.h | ||
---|---|---|
50 | noexcept(is_nothrow_default_constructible_v<_View>)? Also, this should be tested. | |
67 | I think __cached_begin_(nullopt) might be clearer - it's a nice reminder that we're not default-constructing the iterator itself, but only an optional. (here and elsewhere) | |
134 | I had not realized that the WP was indenting requires-clauses, but I think it might make sense to try to follow that. @cjdb is there a reason why you went for no indentation in the first place? What I care most about is that we're consistent and that our code is readable and idiomatic as much as possible. Is there an established way of formatting those in the community? | |
libcxx/test/std/ranges/range.adaptors/range.drop/begin.pass.cpp | ||
14 | Those should describe what's being tested in the test. We've always done that in libc++, and frankly it's quite useful to know exactly what's being tested (especially since we love to have subtly different overloads of functions in the stdlib). |
libcxx/include/__ranges/drop_view.h | ||
---|---|---|
38 | Why does the range being sized matter? Doesn't a random access range provide amortized constant-time complexity for element lookup (which is the requirement for drop_view)? | |
42 | I reordered these based on Tim's suggestion:
But you're right, I think cached_begin should still come first (then count, then base). | |
42 |
Where does it say that private data members go at the bottom? If it's not documented anywhere, I'll go off of what style we currently use. Doing a quick audit of the first three types that come to mind, I see that basic_string, shared_ptr (and all of its helper classes), and map all put their data members at the top. After I continued to look, the only class I could find that didn't do this is vector, but even its base/helper classes put their private members at the top. I find these comments about formatting (here and elsewhere) counter productive. I try not to comment on formatting unless it's really egregious. Until we create some formatting rules (which I think would be a great idea, because then contributors would know how to format their code, and you wouldn't have to waste time in reviews asking for changes), let's not comment about formatting unless it makes the code harder to understand. | |
142–145 | At some point, yes. But we don't have all_t yet and this is not functionally different than what it will be. |
libcxx/include/__ranges/drop_view.h | ||
---|---|---|
38 | You need the size of the underlying view to figure out how much to drop in constant time. If you don't know that, then you have to advance the iterator one step at a time until you either reach the end of the range or N steps, and that's linear. |
libcxx/include/__ranges/drop_view.h | ||
---|---|---|
50 | Why are we not just = defaulting this constructor? | |
134 |
It's the style I adopted back in 2016 when working on cmcstl2; libc++ didn't have any established precedent for requires-clauses, so I paved a way forward. I also find it kind of arbitrary to indent it? We don't indent in the middle of a code block unless we open some sort of grouping tokens (usually parens or braces).
All four use same-line requires-clauses for short requirements, which I think we should adopt in libc++.
Sure, if you want to rescind your offer from D99691, I'll play ball. I would like to know why you think it makes sense to follow the standard wording, but we can follow up with that offline. |
LGTM pending CI and applying the comments you noted when we spoke just now.
This is awesome! Let's move to transform_view now!
libcxx/include/__ranges/drop_view.h | ||
---|---|---|
42 | @zoecarver is right that we sometimes have private data members at the top, and sometimes at the bottom. I think either way's fine. Regarding formatting comments, I think what's hurting is not that we have them during reviews, but that not everybody agrees on how to format things. That creates back-and-forth and arguing over those details, and that is what's harmful. Taking 2 seconds to reformat something that isn't formatted properly isn't the issue IMO. |
libcxx/include/__ranges/drop_view.h | ||
---|---|---|
107 | For the random-access-and-sized case, begin should not call ranges::next - that can be linear for sized-but-not-sized-sentinel ranges. It should just return ranges::begin(__base_) + std::min(__count_, ranges::distance(__base_)). Ditto below. |
libcxx/include/__ranges/drop_view.h | ||
---|---|---|
107 | I think it's OK to use next because we can ensure that next will always use something equivalent to ranges::begin(__base_) + std::min(__count_, ranges::distance(__base_)) (even if it's technically permitted to do something slower by the standard). I'd rather not add a special case here. |
libcxx/include/__ranges/drop_view.h | ||
---|---|---|
107 |
The point is that you can't. For a sized-but-not-sized-sentinel range, the size information is only known from the range itself. Once you decompose the range into an iterator and a sentinel for ranges::next, that information is gone. |
libcxx/include/__ranges/drop_view.h | ||
---|---|---|
107 | I see what you're getting at. So we'd need to change the condition to be sized_sentinel_for (not sized_range) so that it matches what ranges::advance does (or, better yet, do what you suggested and take advantage of the sized range inside of this function). |