On Thu, 27 Feb 2025, Jonathan Wakely wrote: > The specification for std::ranges::iter_move apparently requires us to > handle types which do not satisfy std::indirectly_readable, for example > with overloaded operator* which behaves differently for different value > categories. > > libstdc++-v3/ChangeLog: > > PR libstdc++/106612 > * include/bits/iterator_concepts.h (_IterMove::__iter_ref_t): > New alias template. > (_IterMove::__result): Use __iter_ref_t instead of > std::iter_reference_t. > (_IterMove::__type): Remove incorrect __dereferenceable > constraint. > (_IterMove::operator()): Likewise. Add correct constraints. Use > __iter_ref_t instead of std::iter_reference_t. Forward parameter > as correct value category. > (iter_swap): Add comments. > * testsuite/24_iterators/customization_points/iter_move.cc: Test > that iter_move is found by ADL and that rvalue arguments are > handled correctly. > --- > > Tested x86_64-linux. > > I think the spec is silly to require this, but here we are. > > libstdc++-v3/include/bits/iterator_concepts.h | 33 +++++-- > .../customization_points/iter_move.cc | 95 +++++++++++++++++++ > 2 files changed, 119 insertions(+), 9 deletions(-) > > diff --git a/libstdc++-v3/include/bits/iterator_concepts.h > b/libstdc++-v3/include/bits/iterator_concepts.h > index 4265c475273..555af3bdb38 100644 > --- a/libstdc++-v3/include/bits/iterator_concepts.h > +++ b/libstdc++-v3/include/bits/iterator_concepts.h > @@ -103,32 +103,42 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION > namespace ranges > { > /// @cond undocumented > + // Implementation of std::ranges::iter_move, [iterator.cust.move]. > namespace __imove > { > void iter_move() = delete; > > + // Satisfied if _Tp is a class or enumeration type and iter_move > + // can be found by argument-dependent lookup. > template<typename _Tp> > concept __adl_imove > = (std::__detail::__class_or_enum<remove_reference_t<_Tp>>) > - && requires(_Tp&& __t) { iter_move(static_cast<_Tp&&>(__t)); }; > + && requires(_Tp&& __t) { iter_move(static_cast<_Tp&&>(__t)); }; > > struct _IterMove > { > private: > + // The type returned by dereferencing a value of type _Tp. > + // Unlike iter_reference_t this preserves the value category of _Tp. > + template<typename _Tp> requires requires { *std::declval<_Tp>(); }
IIUC this requires-clause is redundant since it's checking a subset of the alias definition (and alias templates are transparent so if the decltype is invalid it'll induce an error in the immediate context)? > + using __iter_ref_t = decltype(*std::declval<_Tp>()); > + > template<typename _Tp> > struct __result > - { using type = iter_reference_t<_Tp>; }; > + { using type = __iter_ref_t<_Tp>; }; > > + // Use iter_move(E) if that works. > template<typename _Tp> > requires __adl_imove<_Tp> > struct __result<_Tp> > { using type = decltype(iter_move(std::declval<_Tp>())); }; > > + // Otherwise, if *E if an lvalue, use std::move(*E). > template<typename _Tp> > requires (!__adl_imove<_Tp>) > - && is_lvalue_reference_v<iter_reference_t<_Tp>> > + && is_lvalue_reference_v<__iter_ref_t<_Tp>> > struct __result<_Tp> > - { using type = remove_reference_t<iter_reference_t<_Tp>>&&; }; > + { using type = remove_reference_t<__iter_ref_t<_Tp>>&&; }; > > template<typename _Tp> > static constexpr bool > @@ -142,10 +152,11 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION > > public: > // The result type of iter_move(std::declval<_Tp>()) > - template<std::__detail::__dereferenceable _Tp> > + template<typename _Tp> > using __type = typename __result<_Tp>::type; > > - template<std::__detail::__dereferenceable _Tp> > + template<typename _Tp> > + requires __adl_imove<_Tp> || requires { *std::declval<_Tp>(); } Maybe requires { typename __iter_ref_t<_Tp>; } instead to make it more obvious that the __iter_ref_t<_Tp> in the function body is always valid? > [[nodiscard]] > constexpr __type<_Tp> > operator()(_Tp&& __e) const > @@ -153,10 +164,10 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION > { > if constexpr (__adl_imove<_Tp>) > return iter_move(static_cast<_Tp&&>(__e)); > - else if constexpr (is_lvalue_reference_v<iter_reference_t<_Tp>>) With the new constraints it's not clear that iter_reference_t<_Tp> is always valid here, say, if _Tp has an operator* that returns void? > - return static_cast<__type<_Tp>>(*__e); > + else if constexpr (is_lvalue_reference_v<__iter_ref_t<_Tp>>) > + return std::move(*static_cast<_Tp&&>(__e)); > else > - return *__e; > + return *static_cast<_Tp&&>(__e); > } > }; > } // namespace __imove > @@ -167,6 +178,7 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION > } > } // namespace ranges > > + /// The result type of ranges::iter_move(std::declval<_Tp&>()) > template<__detail::__dereferenceable _Tp> > requires > __detail::__can_reference<ranges::__imove::_IterMove::__type<_Tp&>> > using iter_rvalue_reference_t = ranges::__imove::_IterMove::__type<_Tp&>; > @@ -873,11 +885,14 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION > namespace ranges > { > /// @cond undocumented > + // Implementation of std::ranges::iter_swap, [iterator.cust.swap]. > namespace __iswap > { > template<typename _It1, typename _It2> > void iter_swap(_It1, _It2) = delete; > > + // Satisfied if _Tp and _Up are class or enumeration types and iter_swap > + // can be found by argument-dependent lookup. > template<typename _Tp, typename _Up> > concept __adl_iswap > = (std::__detail::__class_or_enum<remove_reference_t<_Tp>> > diff --git > a/libstdc++-v3/testsuite/24_iterators/customization_points/iter_move.cc > b/libstdc++-v3/testsuite/24_iterators/customization_points/iter_move.cc > index c5def5ac577..341bd5b98d7 100644 > --- a/libstdc++-v3/testsuite/24_iterators/customization_points/iter_move.cc > +++ b/libstdc++-v3/testsuite/24_iterators/customization_points/iter_move.cc > @@ -63,8 +63,103 @@ test01() > VERIFY( test_X(3, 4) ); > } > > +template<typename T> > +using rval_ref = std::iter_rvalue_reference_t<T>; > + > +static_assert(std::same_as<rval_ref<int*>, int&&>); > +static_assert(std::same_as<rval_ref<const int*>, const int&&>); > +static_assert(std::same_as<rval_ref<std::move_iterator<int*>>, int&&>); > + > +template<typename T> > +concept iter_movable = requires { std::ranges::iter_move(std::declval<T>()); > }; > + > +struct Iter > +{ > + friend int& iter_move(Iter&) { static int i = 1; return i; } > + friend long iter_move(Iter&&) { return 2; } > + const short& operator*() const & { static short s = 3; return s; } > + friend float operator*(const Iter&&) { return 4.0f; } > +}; > + > +void > +test_adl() > +{ > + Iter it; > + const Iter& cit = it; > + > + VERIFY( std::ranges::iter_move(it) == 1 ); > + VERIFY( std::ranges::iter_move(std::move(it)) == 2 ); > + VERIFY( std::ranges::iter_move(cit) == 3 ); > + VERIFY( std::ranges::iter_move(std::move(cit)) == 4.0f ); > + > + // The return type should be unchanged for ADL iter_move: > + static_assert(std::same_as<decltype(std::ranges::iter_move(it)), int&>); > + static_assert(std::same_as<decltype(std::ranges::iter_move(std::move(it))), > + long>); > + // When ADL iter_move is not used, return type should be an rvalue: > + static_assert(std::same_as<decltype(std::ranges::iter_move(cit)), > + const short&&>); > + > static_assert(std::same_as<decltype(std::ranges::iter_move(std::move(cit))), > + float>); > + > + // std::iter_rvalue_reference_t always considers the argument as lvalue. > + static_assert(std::same_as<rval_ref<Iter>, int&>); > + static_assert(std::same_as<rval_ref<Iter&>, int&>); > + static_assert(std::same_as<rval_ref<const Iter>, const short&&>); > + static_assert(std::same_as<rval_ref<const Iter&>, const short&&>); > +} > + > +void > +test_pr106612() > +{ > + // Bug 106612 ranges::iter_move does not consider iterator's value > categories > + > + struct I > + { > + int i{}; > + int& operator*() & { return i; } > + int operator*() const & { return i; } > + void operator*() && = delete; > + }; > + > + static_assert( iter_movable<I&> ); > + static_assert( iter_movable<I const&> ); > + static_assert( ! iter_movable<I> ); > + static_assert( std::same_as<std::iter_rvalue_reference_t<I>, int&&> ); > + static_assert( std::same_as<std::iter_rvalue_reference_t<const I>, int> ); > + > + struct I2 > + { > + int i{}; > + int& operator*() & { return i; } > + int operator*() const & { return i; } > + void operator*() &&; > + }; > + > + static_assert( iter_movable<I2&> ); > + static_assert( iter_movable<I2 const&> ); > + static_assert( iter_movable<I2> ); > + static_assert( std::is_void_v<decltype(std::ranges::iter_move(I2{}))> ); > + static_assert( std::same_as<std::iter_rvalue_reference_t<I2>, int&&> ); > + static_assert( std::same_as<std::iter_rvalue_reference_t<I2 const>, int> ); > + > + enum E { e }; > + enum F { f }; > + > + struct I3 > + { > + E operator*() const & { return e; } > + F operator*() && { return f; } > + }; > + > + static_assert( iter_movable<I3&> ); > + static_assert( iter_movable<I3> ); > + static_assert( std::same_as<decltype(std::ranges::iter_move(I3{})), F> ); > +} > + > int > main() > { > test01(); > + test_adl(); > } > -- > 2.48.1 > >