[PATCH] libstdc++: Fix ranges::iter_move handling of rvalues [PR106612]
Patrick Palka
ppalka@redhat.com
Fri Feb 28 21:27:17 GMT 2025
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
>
>
More information about the Libstdc++
mailing list