3448. transform_view's sentinel<false> not comparable with iterator<true>

Section: 25.7.9.4 [range.transform.sentinel], 25.7.14.4 [range.join.sentinel] Status: C++23 Submitter: Jonathan Wakely Opened: 2020-05-26 Last modified: 2023-11-22

Priority: 1

View all other issues in [range.transform.sentinel].

View all issues with C++23 status.

Discussion:

A user reported that this doesn't compile:

#include <list>
#include <ranges>

std::list v{1, 2}; // works if std::vector
auto view1 = v | std::views::take(2);
auto view2 = view1 | std::views::transform([] (int i) { return i; });
bool b = std::ranges::cbegin(view2) == std::ranges::end(view2);

The comparison is supposed to use operator==(iterator<Const>, sentinel<Const>) after converting sentinel<false> to sentinel<true>. However, the operator== is a hidden friend so is not a candidate when comparing iterator<true> with sentinel<false>. The required conversion would only happen if we'd found the operator, but we can't find the operator until after the conversion happens.

As Patrick noted, the join_view sentinel has a similar problem.

The proposed wording shown below has been suggested by Casey and has been implemented and tested in GCC's libstdc++.

[2020-07-17; Priority set to 1 in telecon]

Should be considered together with 3406 and 3449.

Previous resolution [SUPERSEDED]:

This wording is relative to N4861.

  1. Modify 25.2 [ranges.syn], header <ranges> synopsis, as indicated:

    [Drafting note: The project editor is kindly asked to consider replacing editorially all of the

    "using Base = conditional_t<Const, const V, V>;"

    occurrences by

    "using Base = maybe-const<Const, V>;"

    ]

    […]
    namespace std::ranges {
    […]
      namespace views { inline constexpr unspecified filter = unspecified; }
      
      template<bool Const, class T>
        using maybe-const = conditional_t<Const, const T, T>; // exposition-only
      
      // 25.7.9 [range.transform], transform view
      template<input_range V, copy_constructible F>
        requires view<V> && is_object_v<F> &&
                 regular_invocable<F&, range_reference_t<V>>
      class transform_view;
    […]
    }
    
  2. Modify 25.7.9.4 [range.transform.sentinel], class template transform_view::sentinel synopsis, as indicated:

    namespace std::ranges {
      template<input_range V, copy_constructible F>
        requires view<V> && is_object_v<F> &&
                 regular_invocable<F&, range_reference_t<V>> &&
                 can-reference<invoke_result_t<F&, range_reference_t<V>>>
      template<bool Const>
      class transform_view<V, F>::sentinel {
        […]
        constexpr sentinel_t<Base> base() const;
        
        template<bool OtherConst>
          requires sentinel_for<sentinel_t<Base>, iterator_t<maybe-const<OtherConst, V>>> 
        friend constexpr bool operator==(const iterator<OtherConst>& x, const sentinel& y);
        
        template<bool OtherConst>
          requires sized_sentinel_for<sentinel_t<Base>, iterator_t<maybe-const<OtherConst, V>>> 
        friend constexpr range_difference_t<Base>
          operator-(const iterator<OtherConst>& x, const sentinel& y)
            requires sized_sentinel_for<sentinel_t<Base>, iterator_t<Base>>;
    
        template<bool OtherConst>
          requires sized_sentinel_for<sentinel_t<Base>, iterator_t<maybe-const<OtherConst, V>>> 
        friend constexpr range_difference_t<Base>
          operator-(const sentinel& y, const iterator<OtherConst>& x)
            requires sized_sentinel_for<sentinel_t<Base>, iterator_t<Base>>;
      };
    }
    
    […]
    template<bool OtherConst>
      requires sentinel_for<sentinel_t<Base>, iterator_t<maybe-const<OtherConst, V>>> 
    friend constexpr bool operator==(const iterator<OtherConst>& x, const sentinel& y);
    

    -4- Effects: Equivalent to: return x.current_ == y.end_;

    template<bool OtherConst>
      requires sized_sentinel_for<sentinel_t<Base>, iterator_t<maybe-const<OtherConst, V>>> 
    friend constexpr range_difference_t<Base>
      operator-(const iterator<OtherConst>& x, const sentinel& y)
        requires sized_sentinel_for<sentinel_t<Base>, iterator_t<Base>>;
    

    -5- Effects: Equivalent to: return x.current_ - y.end_;

    template<bool OtherConst>
      requires sized_sentinel_for<sentinel_t<Base>, iterator_t<maybe-const<OtherConst, V>>> 
    friend constexpr range_difference_t<Base>
      operator-(const sentinel& y, const iterator<OtherConst>& x)
        requires sized_sentinel_for<sentinel_t<Base>, iterator_t<Base>>;
    

    -6- Effects: Equivalent to: return y.end_ - x.current_;

  3. Modify 25.7.14.4 [range.join.sentinel], class template join_view::sentinel synopsis, as indicated:

    namespace std::ranges {
      template<input_range V>
        requires view<V> && input_range<range_reference_t<V>> &&
                 (is_reference_v<range_reference_t<V>> ||
                 view<range_value_t<V>>)
      template<bool Const>
      class join_view<V>::sentinel {
        […]
    
        template<bool OtherConst>
          requires sentinel_for<sentinel_t<Base>, iterator_t<maybe-const<OtherConst, V>>>
        friend constexpr bool operator==(const iterator<OtherConst>& x, const sentinel& y);  
    };
    }
    
    […]
    template<bool OtherConst>
      requires sentinel_for<sentinel_t<Base>, iterator_t<maybe-const<OtherConst, V>>>
    friend constexpr bool operator==(const iterator<OtherConst>& x, const sentinel& y);  
    

    -3- Effects: Equivalent to: return x.outer_ == y.end_;

[2020-08-21 Tim updates PR per telecon discussion]

As noted in the PR of LWG 3406, the return type of operator- should be based on the constness of the iterator rather than that of the sentinel, as sized_sentinel_for<S, I> (24.3.4.8 [iterator.concept.sizedsentinel]) requires decltype(i - s) to be iter_difference_t<I>.

[2020-10-02; Status to Tentatively Ready after five positive votes on the reflector]

[2020-11-09 Approved In November virtual meeting. Status changed: Tentatively Ready → WP.]

Proposed resolution:

This wording is relative to N4861.

  1. Modify 25.2 [ranges.syn], header <ranges> synopsis, as indicated:

    [Drafting note: The project editor is kindly asked to consider replacing editorially all of the

    "using Base = conditional_t<Const, const V, V>;"

    occurrences by

    "using Base = maybe-const<Const, V>;"

    ]

    […]
    namespace std::ranges {
    […]
      namespace views { inline constexpr unspecified filter = unspecified; }
      
      template<bool Const, class T>
        using maybe-const = conditional_t<Const, const T, T>; // exposition-only
      
      // 25.7.9 [range.transform], transform view
      template<input_range V, copy_constructible F>
        requires view<V> && is_object_v<F> &&
                 regular_invocable<F&, range_reference_t<V>>
      class transform_view;
    […]
    }
    
  2. Modify 25.7.9.4 [range.transform.sentinel], class template transform_view::sentinel synopsis, as indicated:

    namespace std::ranges {
      template<input_range V, copy_constructible F>
        requires view<V> && is_object_v<F> &&
                 regular_invocable<F&, range_reference_t<V>> &&
                 can-reference<invoke_result_t<F&, range_reference_t<V>>>
      template<bool Const>
      class transform_view<V, F>::sentinel {
        […]
        constexpr sentinel_t<Base> base() const;
        
        template<bool OtherConst>
          requires sentinel_for<sentinel_t<Base>, iterator_t<maybe-const<OtherConst, V>>> 
        friend constexpr bool operator==(const iterator<OtherConst>& x, const sentinel& y);
        
        template<bool OtherConst>
          requires sized_sentinel_for<sentinel_t<Base>, iterator_t<maybe-const<OtherConst, V>>> 
        friend constexpr range_difference_t<Basemaybe-const<OtherConst, V>>
          operator-(const iterator<OtherConst>& x, const sentinel& y)
            requires sized_sentinel_for<sentinel_t<Base>, iterator_t<Base>>;
    
        template<bool OtherConst>
          requires sized_sentinel_for<sentinel_t<Base>, iterator_t<maybe-const<OtherConst, V>>> 
        friend constexpr range_difference_t<Basemaybe-const<OtherConst, V>>
          operator-(const sentinel& y, const iterator<OtherConst>& x)
            requires sized_sentinel_for<sentinel_t<Base>, iterator_t<Base>>;
      };
    }
    
    […]
    template<bool OtherConst>
      requires sentinel_for<sentinel_t<Base>, iterator_t<maybe-const<OtherConst, V>>> 
    friend constexpr bool operator==(const iterator<OtherConst>& x, const sentinel& y);
    

    -4- Effects: Equivalent to: return x.current_ == y.end_;

    template<bool OtherConst>
      requires sized_sentinel_for<sentinel_t<Base>, iterator_t<maybe-const<OtherConst, V>>> 
    friend constexpr range_difference_t<Basemaybe-const<OtherConst, V>>
      operator-(const iterator<OtherConst>& x, const sentinel& y)
        requires sized_sentinel_for<sentinel_t<Base>, iterator_t<Base>>;
    

    -5- Effects: Equivalent to: return x.current_ - y.end_;

    template<bool OtherConst>
      requires sized_sentinel_for<sentinel_t<Base>, iterator_t<maybe-const<OtherConst, V>>> 
    friend constexpr range_difference_t<Basemaybe-const<OtherConst, V>>
      operator-(const sentinel& y, const iterator<OtherConst>& x)
        requires sized_sentinel_for<sentinel_t<Base>, iterator_t<Base>>;
    

    -6- Effects: Equivalent to: return y.end_ - x.current_;

  3. Modify 25.7.14.4 [range.join.sentinel], class template join_view::sentinel synopsis, as indicated:

    namespace std::ranges {
      template<input_range V>
        requires view<V> && input_range<range_reference_t<V>> &&
                 (is_reference_v<range_reference_t<V>> ||
                 view<range_value_t<V>>)
      template<bool Const>
      class join_view<V>::sentinel {
        […]
    
        template<bool OtherConst>
          requires sentinel_for<sentinel_t<Base>, iterator_t<maybe-const<OtherConst, V>>>
        friend constexpr bool operator==(const iterator<OtherConst>& x, const sentinel& y);  
    };
    }
    
    […]
    template<bool OtherConst>
      requires sentinel_for<sentinel_t<Base>, iterator_t<maybe-const<OtherConst, V>>>
    friend constexpr bool operator==(const iterator<OtherConst>& x, const sentinel& y);  
    

    -3- Effects: Equivalent to: return x.outer_ == y.end_;