As discussed with ldionne. The problem with this static_assert is that it makes ranges::begin a pitfall for anyone ever to use inside a constraint or decltype. Many Ranges things, such as ranges::size, are specified as "Does X if X is well-formed, or else Y if Y is well-formed, or else `ranges::end(t) - ranges::begin(t)` if that is well-formed, or else..." And if there's a static_assert hidden inside `ranges::begin(t)`, then you get a hard error as soon as you ask the question -- even if the answer would have been "no, that's not well-formed"! Constraining on `requires { t + 0; }` or `requires { t + N; }` is verboten because of https://gcc.gnu.org/bugzilla//show_bug.cgi?id=103700 . For ranges::begin, we can just decay to a pointer even in the incomplete-type case. For ranges::end, we can safely constrain on `sizeof(*t)`. Yes, this means that an array of incomplete type has a `ranges::begin` but no `ranges::end`... just like an unbounded array of complete type. This is a valid manifestation of IFNDR. All of the new libcxx/test/std/ cases are mandatory behavior, as far as I'm aware. Tests for the IFNDR cases in ranges::begin and ranges::end remain in `libcxx/test/libcxx/`. The similar tests for ranges::empty and ranges::data were simply wrong, AFAIK.
Details
- Reviewers
ldionne - Group Reviewers
Restricted Project - Commits
- rG8ad364ad2123: [libc++] [ranges] Remove the static_assert from ranges::begin and ranges::end.
Diff Detail
- Repository
- rG LLVM Github Monorepo
Event Timeline
libcxx/include/__ranges/access.h | ||
---|---|---|
58–59 | This may be a bad comment. At least it's inconsistent between "t" and "__t". | |
120–124 | I needed to add this requires (or, I could add -> decltype(__t + _Np) if you'd rather ;)) because otherwise this hard-errors when you try to call it on an array of incomplete type. | |
libcxx/test/libcxx/ranges/range.access/range.prim/data.incomplete.verify.cpp | ||
23 | This is calling ranges::data on a pointer variable. The expected-error here is surprising, but I didn't look into it. | |
35 | If this were std::move(arr), then it would be required to SFINAE away, not hard-error. But it's not. | |
libcxx/test/libcxx/ranges/range.access/range.prim/empty.incomplete.verify.cpp | ||
24 | Every test in this file tests ranges::begin, when the test file is named empty.incomplete.verify.cpp. |
Indeed it was as I feared, re that GCC bug with arrays of incomplete types. https://reviews.llvm.org/harbormaster/unit/view/1587680/
I think I have the workaround now, though.
I would have expected a test for SFINAE-friendliness of ranges::data on incomplete array types, but http://eel.is/c++draft/range.access#range.prim.data-2.2 says it's IFNDR. These inconsistencies in the spec are somewhat concerning.
libcxx/include/__ranges/access.h | ||
---|---|---|
60 | Could we add another overload like this: template <class _Tp> requires is_array_v<remove_cv_t<_Tp>> && !__is_complete<iter_value_t<_Tp>> void operator()(_Tp&&) = delete; of something of that sort? Otherwise, why don't we simply add the constraint __is_complete to this overload? | |
121 | Why not use __is_complete? The name conveys more information than this expression IMO. |
libcxx/include/__ranges/access.h | ||
---|---|---|
60 | Here, I'm intentionally permitting ranges::begin(x) to return x when x is an array of incomplete type, because that seems strictly more useful than any other behavior (and it's IFNDR so we get to pick our behavior). Notice that if the type of *x becomes complete, later in the TU, we haven't violated the ODR yet. If we did what you suggest above, and made the behavior of this function dependent on the completeness of *x, then we would be violating the ODR. See my comment on ranges::end also; that rationale also applies here. But in the ranges::end case, we don't have the option to choose "Don't violate the ODR"; there we must simply try to minimize the collateral damage. | |
121 | I think it's valuable for us to stay away from any named entity that might be cached by the compiler — no type trait, no concept. The only thing we have to commit to (and maybe get cached) on this line is the well-formedness of ranges::end. We don't want this to have spooky effects on the well-formedness of other unrelated things as compilation of this TU proceeds (which is what would happen if we shared a dependency on __is_complete or whatever). |
We seem to be missing a libc++ specific test for the extension that we return the start of the array when calling ranges::begin(incomplete).
LGTM except for that nit and my other comment.
libcxx/include/__ranges/access.h | ||
---|---|---|
120–124 | I think I'd rather use -> decltype(__t + _Np) and not acknowledge explicitly that we're treating incomplete types any special way here. |
libcxx/include/__ranges/access.h | ||
---|---|---|
120–124 | My above comment dates to an earlier version of the diff. IIRC, requires requires { __t + _Np; } did indeed hit https://gcc.gnu.org/bugzilla//show_bug.cgi?id=103700 , and I believe -> decltype(__t + _Np) would also hit it. I'll upload the diff again just to make sure I'm right about that (and also I'll add the new libcxx/test/libcxx/ test you asked for). If I'm right, then I'll have to continue using requires (sizeof(*__t) != 0) here. Because of https://gcc.gnu.org/bugzilla//show_bug.cgi?id=103700 , the constraint will have to be something that doesn't depend on pointer arithmetic. (Test case from 103700 demonstrating GCC 11's problem with pointer arithmetic: https://godbolt.org/z/G7e9hx5cb ) |
libcxx/test/libcxx/ tests.
Reintroduce decltype(__t + _Np) to prove it really doesn't work with GCC 11. Expect GCC CI to fail.
libcxx/include/__ranges/access.h | ||
---|---|---|
120–124 | Indeed I was right about that. Using template <class _Tp, size_t _Np> [[nodiscard]] _LIBCPP_HIDE_FROM_ABI constexpr auto operator()(_Tp (&__t)[_Np]) const noexcept -> decltype(__t + _Np) { return __t + _Np; } we hit the GCC 11 bug: https://buildkite.com/llvm-project/libcxx-ci/builds/7339#e2494f74-3397-43d8-aa24-326e0275dd39 template <class _Tp, size_t _Np> [[nodiscard]] _LIBCPP_HIDE_FROM_ABI constexpr auto operator()(_Tp (&__t)[_Np]) const noexcept requires (sizeof(*__t) != 0) { return __t + _Np; } , wait to confirm that CI is green again, and land this, unless @ldionne interrupts that plan. :) |
Revert to requires (sizeof(*__t) != 0). Will land if CI is happy with this (which I expect it will be).
Grr, it turns out that GCC (due to a lower optimization setting?) is unhappy with libcxx/test/libcxx/ranges/range.access/begin.incomplete.type.pass.cpp because we want to link and run the test but it also contains undefined references to extern arrays.
libcxx/include/__ranges/access.h | ||
---|---|---|
120–124 | At least, please add a comment like // make sure _Tp is complete |
libcxx/include/__ranges/access.h | ||
---|---|---|
120–124 | I feel like my super-verbose commit message should be enough... but OK, I've added a comment here. |
Use the TU1/TU2 approach suggested by Louis for begin.incomplete.type.pass.cpp.
Add a missing test case for ranges::begin(unbounded-array-of-complete-type), which is not IFNDR; it just has to work.
...And remove the new test cases for ranges::begin(static_cast<int(&)[]>(a)) and so on, again. These casts are new in C++20, so they're also new in Clang 14: Clang 12 and 13 fail the new test. So, remove them so I can land this, and then we can make a new test file for these few cases after that.
This may be a bad comment. At least it's inconsistent between "t" and "__t".