Document number P3741R1
Date 2026-06-30
Audience LEWG, SG9 (Ranges)
Reply-to Hewill Kang <hewillk@gmail.com>

views::set_operations

Abstract

This paper proposes four range adaptors for set operations rated as Tier 3 in P2760, namely views::set_difference, views::set_intersection, views::set_union, and views::set_symmetric_difference, to extend the breadth of Ranges.

These adaptors complement existing set algorithms by enabling composable, lazy, and allocation-free views for common set operations. They fill a notable gap in the Ranges library for many practical applications.

Revision history

R0

Initial revision.

R1 (based on SG9's feedback in Brno)

1. Support custom comparator.

2. Provide reserve_hint member.

3. Support variadic template version.

4. Specify EB (Erroneous Behavior) when the range is not sorted.

Motivation

Although there are corresponding constrained algorithm versions of set operations, they all need to output the results to some sort of output range. This brings advantages of the view's lazy evaluation: we can construct set elements on the fly without allocating memory in advance.

Given that set operations are extremely common in the real world, introducing corresponding range adaptors is valuable and facilitates the user experience with Ranges:

  /* algorithm approach */
  std::vector<int> dest;
  ranges::set_union(v1, v2, std::back_inserter(dest));

  /* no allocation, composable, n-ary */
  auto union = views::set_union(v1, v2, v3, ...);

Design

Pipe syntax is Not support

The union and intersection operations support variadic templates, which requires them to be implemented as Customization Point Objects (CPOs) rather than range adaptors.

This design inherently precludes pipe syntax. Even if they were adaptors, pipe syntax would create ambiguity. For instance, in an expression like views::set_union(r2, r3), it is unclear whether views::set_union(r2, r3) is intended to be a range adaptor expecting r1 as input, or a view factory that computes the union of r2 and r3.

We have also decided against pipe syntax for other two. Set operations generally require sorted ranges; since a range passed through a pipe is rarely guaranteed to be sorted, users would likely need to sort it manually first. Given that we currently lack eager algorithms like action::sort to facilitate this within a pipeline, the utility of pipe syntax for these operations is limited. We believe that maintaining a consistent API design across all set operations is preferable.

Constraint for intersection / difference

Intersection A B Difference A B

For views::set_operations(A, B), both intersection and difference produce only elements from A, so the result is just a subset of A; the element type of B is irrelevant for output, only its order matters. The only thing that matters is making sure the elements of both ranges are strictly weakly ordered so that they can be compared meaningfully.

The standard already has a concept for this, namely indirect_strict_weak_order, which is also used for the corresponding constrained algorithm (a component of mergeable concept). The signatures of the two classes would be:

 template<view V1, view V2>
    requires input_range<V1> && input_range<V2> &&
             indirect_strict_weak_order<ranges::less, iterator_t<V1>, iterator_t<V2>>
  class set_meow_view;

Constraint for union / symmetric difference

Union A B Symmetric Difference A B

Unlike the above, union and symmetric difference produce elements from both A and B. In addition to ensuring that both ranges are strictly weakly ordered, we also need to ensure that the element types of both ranges are somehow compatible.

Thanks to the fact that it is already concat_view in the standard, the concatable concept is perfect for such a purpose. The signatures of both would be:

  template<view V1, view V2>
    requires input_range<V1> && input_range<V2> &&
             indirect_strict_weak_order<ranges::less, iterator_t<V1>, iterator_t<V2>> &&
             concatable<V1, V2>
  class set_meow_view;

Iterator design for intersection / difference

The iterators of the four views all follow a similar design. When they are constructed, we first find the next valid element through the satisfy() function, and then in each operator++(), we increment the underlying iterator and call the satisfy() again to find the next valid element, and so on.

Since in satisfy(), we also need to check whether the iterators of A or B have reached the end to determine the next valid element, we need to know the information of the sentinels of both, which means we need to store both sentinels in the iterator.

For set_intersection_view, its iterator signature is as follows:

  class set_intersection_view::iterator {
    iterator_t<V1> current1_;
    sentinel_t<V1> end1_;
    iterator_t<V2> current2_;
    sentinel_t<V2> end2_;

    constexpr void
    satisfy() {
      while (current1_ != end1_ && current2_ != end2_) {
         /* Find the next valid element in the first range */ 
      }
    }

    constexpr iterator(iterator_t<V1> current1, sentinel_t<V1> end1,
                       iterator_t<V2> current2, sentinel_t<V2> end2)
      : current1_(std::move(current1)), end1_(end1),
        current2_(std::move(current2)), end2_(end2) {
      satisfy();
    } 

  public:
    constexpr decltype(auto) operator*() const { return *current1_; }

    constexpr iterator&
    operator++() {
      ++current1_;
      ++current2_;
      satisfy();
      return *this;
    }

    friend constexpr bool
    operator==(const iterator& x, default_sentinel_t) {
      return x.current1_ == x.end1_ || x.current2_ == x.end2_;
    }
  };
  
We can slightly modify the logic of the three functions above, satisfy(), operator++(), and operator==(), to make a corresponding iterator for set_difference_view:
  class set_difference_view::iterator {
    iterator_t<V1> current1_;
    sentinel_t<V1> end1_;
    iterator_t<V2> current2_;
    sentinel_t<V2> end2_;

    constexpr void
    satisfy() {
      while (current1_ != end1_ && current2_ != end2_) {
        /* New condition to find the next valid element in the first range */
      }
    }

    constexpr iterator(iterator_t<V1> current1, sentinel_t<V1> end1,
                       iterator_t<V2> current2, sentinel_t<V2> end2)
      : current1_(std::move(current1)), end1_(end1),
        current2_(std::move(current2)), end2_(end2) {
      satisfy();
    } 

  public:
    constexpr decltype(auto) operator*() const { return *current1_; }

    constexpr iterator&
    operator++() {
      ++current1_;
      /* ++current2_; */
      satisfy();
      return *this;
    }

    friend constexpr bool
    operator==(const iterator& x, default_sentinel_t) {
      return x.current1_ == x.end1_ /* || x.current2_ == x.end2_*/;
    }
  };
    

Iterator design for union / symmetric_difference

For set_union_view's iterator, it is necessary to know which underlying iterator is active right now, so an additional flag is need to indicate that.

In addition, since the resulting set contains elements from two different ranges, the new reference type needs to be a common reference of the two, in which case concat-reference-t nicely fits the purpose:

    class set_union_view::iterator {
      iterator_t<V1> current1_;
      sentinel_t<V1> end1_;
      iterator_t<V2> current2_;
      sentinel_t<V2> end2_;
      size_t active_idx_;
  
      constexpr void
      satisfy() {
        /* Find the next valid element from two ranges */
      }
  
      constexpr iterator(iterator_t<V1> current1, sentinel_t<V1> end1,
                         iterator_t<V2> current2, sentinel_t<V2> end2)
        : current1_(std::move(current1)), end1_(end1),
          current2_(std::move(current2)), end2_(end2) {
        satisfy();
      } 
  
    public:
      constexpr concat-reference-t<V1, V2>
      operator*() const {
        if (active_idx_ == 0)
          return *current1_;
        return *current2_;
      }

      constexpr iterator&
      operator++() {
        if (active_idx_ == 0)
          ++current1_;
        else
          ++current2_;
        satisfy();
        return *this;
      }
  
      friend constexpr bool
      operator==(const iterator& x, default_sentinel_t) {
        return x.current1_ == x.end1_ && x.current2_ == x.end2_;
      }
    };
      
Similarly, we can make set_symmetric_difference_view's iterator by modifying satisfy() to skip the invalid part.

Variadic version for intersection/union

During the Brno, SG9 suggested exploring variadic template versions for these views to increase flexibility. However, after further technical consideration, we believe that only intersection and union are actually worth having variadic template versions because they are inherently less ambiguous. Making the other two into variadic templates is neither reasonable nor worthwhile.

The reason only first two are viable comes down to their mathematical properties. Both are strictly associative and commutative. For instance, an expression like (A ∪ B) ∪ C yields the exact same result as A ∪ (B ∪ C). Under multiset semantics, evaluating the first grouping yields a final element frequency of max(max(a, b), c), while the second yields max(a, max(b, c)). Since the max operation is associative, both groupings produce an identical range.

The exact same logic applies to intersection, where an expression like (A ∩ B) ∩ C is identical to A ∩ (B ∩ C). The final frequency for both groupings is a deterministic min(min(a, b), c) = min(a, min(b, c)). Because both operations yield identical element frequencies and order, a variadic version eliminates ambiguity around evaluation order.

Union (A ∪ B ∪ C) A B C Intersection (A ∩ B ∩ C) A B C

Note that under the variadic mathematical definition, a unary set_union or set_intersection simply yields the range itself.

In stark contrast, set difference is non-associative. An expression like (A \ B) \ C yields a completely different result than A \ (B \ C), because the former removes elements found in either B or C from A, while the latter removes elements from A that are in B but not in C. A variadic API would force users to guess which evaluation order the library implements.

Similarly, while symmetric difference is associative,meaning (A Δ B) Δ C does equal A Δ (B Δ C), adding a variadic version just is not worth the implementation complexity. In a multi-range context, a variadic symmetric difference mathematically evaluates to elements that appear an odd number of times. This is a highly niche mathematical artifact; in real-world engineering, developers are usually looking for elements that appear exactly once across all ranges, making this behavior counter-intuitive and practically useless.

Difference ((A \ B) \ C) A B C Symmetric Diff (A Δ B Δ C) A B C

Constraint for Variadic version for intersection/union

For the variadic version of set_union, a pairwise constraint is strictly required because the output contains elements from all ranges. Any element may need to be compared with any other in downstream operations:

// Pack... must be pairwise compatible
template<view... Vs>
  requires (input_range<Vs> && ...) &&
           (indirect_strict_weak_order<ranges::less, iterator_t<Vi>, iterator_t<Vj>> /* for every i, j */) &&
           concatable<Vs...>
class set_union_view;

For set_intersection, the implementation technically only requires comparing the first range V1 against all subsequent ranges, since the output is strictly a subset of V1:

// Implementation minimum requirement
template<view V1, view... Vs>
  requires input_range<V1> && (input_range<Vs> && ...) &&
           (indirect_strict_weak_order<ranges::less, iterator_t<V1>, iterator_t<Vs>> && ...)
class set_intersection_view;

However, from a theoretical standpoint, intersection is fundamentally commutative and associative, meaning that a valid relationship exists between all elements regardless of their order. To respect this mathematical property, set_intersection should adopt the same pairwise constraint as set_union.

This also avoid confusing behavior in practice. Without pairwise constraints, an expression like set_intersection(A, B, C) might compile, but changing it to set_intersection(B, A, C) would suddenly fail just because B and C cannot be compared directly. Ordering the arguments differently shouldn't break the build, so aligning the constraints ensures consistent behavior.

Customized comparison is support

While range/v3 supports both custom comparisons and projections like views::set_operations(rng1, rng2, pred, proj1, proj2), we believe only supporting a custom comparison is reasonable, since several views like views::filter or views::chunk_by already take custom predicates.

On the other hand, supporting custom projections does not really fit the direction of the Ranges library, as no range adaptor supports them. This feature offers little practical usability improvement, considering users can just use views::transform to achieve equivalent behavior. Instead, adding projections forces the view class to store extra fields for the projections, which introduces a heavyweight cost.

Furthermore, supporting projections gets ugly once we move to variadic templates, which would end up with a messy interface like views::set_operations(pred, proj1, proj2, proj3..., r1, r2, r3...). Nobody wants to write or read that.

Borrowed ranges is Not support

Supporting a custom comparison also impacts whether these views can be borrowed ranges. If a custom comparison object is provided, it must be stored within the view class itself. Consequently, the iterator needs to maintain a pointer back to its parent view to access this comparator. Because the iterator holds a dependency on the view's member, these views cannot model borrowed_range .

According to P3117R1 Extending Conditionally Borrowed, if the comparator is a tiny-func, the iterator does not need a parent pointer because it can construct the comparator on the fly. This theoretically allows us to support borrowed_range.

However, without a parent pointer, the iterator must independently store the end iterators of all underlying ranges to safely drive its traversal logic. For non-variadic ones, this requires storing 2 extra end iterators inside the iterator class. For a variadic version, the iterator would have to carry a whole collection of end iterators. This massive size increase makes the iterator too heavyweight, meaning supporting borrowed_range this way is simply not worth the cost.

Common range is Not support

The fundamental reason these views cannot universally support common_range is an implementation impossibility; the exact end positions of the underlying iterators cannot be known in advance. For example, set_difference(r1, r2) finishes the moment r1 reaches its end. But at that exact moment, where should the iterator for r2 be pointing? Its final position depends entirely on the runtime data.

The same fundamental issue applies to set_intersection. The traversal stops as soon as any underlying range hits its end, meaning we can never predict which one will exhaust first, nor where the remaining iterators will be sitting at that moment.

On the other hand, set_union and set_symmetric_difference require fully exhausting all participating ranges to complete their traversal, providing common_range support specifically for these two views is feasible. The paper does currenly not to provide any to keep the design simple and consistent.

Bidirectional is Not support

Theoretically, these views could support bidirectional iteration, but doing so provides little value. For set_difference and set_intersection, it is only known that one range has reached its end, while the final positions of the other ranges remain unknown. Consequently, these views cannot model common_range, which makes supporting bidirectional_range meaningless because it is impossible to execute --view.end().

For set_union and set_symmetric_difference, reverse traversal cannot be correctly supported without tracking the traversal history when the underlying ranges contain duplicate elements:

// Input ranges
{ 2, 3, [4] }
{ 2, 3, (4), <4> }

// Forward Traversal for set_union:
{ 2, 3, [4], <4> }
// Final state: r1_it == r1.end(), r2_it == r2.end()

// Reverse Traversal from set_union:
{ [4], (4), 3, 2 }  // <-- Wrong elements

It is also worth noting that supporting bidirectional iteration would introduce potential use-after-move issues due to element comparisons between the two ranges. For instance, composing views::as_rvalue | views::reverse could lead to undefined behavior.

Therefore, the author does not plan to support bidirectional iteration, which is consistent with the design of range/v3.

Non const-iterable (even for pure input-range)

Except for set union, satisfy() for other operations has the worst complexity of O(n) because we need to skip the invalid white area to locate the next valid element. In order to ensure the amortized constant time complexity of begin() required by the range concept, we need to cache the iterator in the first call to begin(), which means that other view classes except set_union_view are not const-iterable.

It is worth noting that while C++26 introduced const-iterable support for filter_view when the underlying range is an pure input range, a similar optimization is unnecessary for our views. Although input-only ranges do not require iterator caching to maintain complexity guarantees, all set operations fundamentally require their input sequences to be sorted.

Since pre-sorted sequence in an input-only context is exceedingly rare in practical scenarios, adding extra implementation complexity to support a non-caching const begin() for this niche edge case provides no real-world motivation. Therefore, the author chose not to provide such support, keeping the design pragmatic and clean.

Here is the summary table:

View const-Iterable Caches begin() Complexity
set_difference_view Amortized constant
set_intersection_view Amortized constant
set_union_view Constant
set_symmetric_difference_view Amortized constant

Provide reserve_hint() member

During the Brno meeting, SG9 suggested that providing a reserve_hint member function would still be valuable, even if it only offers a loose upper bound. The primary motivation is to allow downstream operations, such as ranges::to<std::vector>(), to pre-allocate memory and reduce the overhead of frequent reallocations.

The implementation logic for computing these upper bounds is straightforward. For a union or a symmetric difference, the maximum possible size is simply the sum of both range sizes, or size(A) + size(B). For an intersection, the resulting size can never exceed the size of the smaller range, which is std::min(size(A), size(B)). Lastly, for a set difference, the size of the output is strictly bounded by the size of the first range, size(A).

No customized iter_swap() specializations

It doesn't make sense to provide iter_swap specializations for these new iterators, since we'd break the origin order by swapping the elements which leads to undefined behavior.

Erroneous behavior for unsorted input ranges

Following feedback from SG9, violating the sorted precondition for input ranges is designated as Erroneous Behavior rather than Undefined Behavior.

While set operations require both input ranges to be sorted to ensure correct semantics, treating unsorted inputs as erroneous behavior strikes the right balance, which allows implementations to catch these contract violations.

Provide base() member

intersection and difference can be seen as a subset of the first range, so it makes sense to provide base() members for it and its iterator to access the first underlying view and iterator.

Implementation experience

The author implemented four views::set_operationss based on libstdc++, see godbolt.

Wording

This wording is relative to the latest working draft.

    1. Add one new feature-test macro to 17.3.2 [version.syn]:

      #define __cpp_lib_ranges_set_view 20XXXXL // freestanding, also in <ranges>
    2. Modify 25.2 [ranges.syn], Header <ranges> synopsis, as indicated:

      #include <compare>              // see [compare.syn]
      #include <initializer_list>     // see [initializer.list.syn]
      #include <iterator>             // see [iterator.synopsis]
      
      namespace std::ranges {
        […]
        namespace views { inline constexpr unspecified to_input = unspecified; }
      
        // [range.set.intersection], set intersection view
        template<class Comp, input_range... Views>
          requires see below
        class set_intersection_view;
      
        namespace views { inline constexpr unspecified set_intersection = unspecified; }
      
        // [range.set.union], set union view
        template<class Comp, input_range... Views>
          requires see below
        class set_union_view;
      
        namespace views { inline constexpr unspecified set_union = unspecified; }
      
        // [range.set.difference], set difference view
        template<class Comp, input_range V1, input_range V2>
          requires see below
        class set_difference_view;
      
        namespace views { inline constexpr unspecified set_difference = unspecified; }
      
        // [range.set.symmetric.difference], set symmetric difference view
        template<class Comp, input_range V1, input_range V2>
          requires see below
        class set_symmetric_difference_view;
      
        namespace views { inline constexpr unspecified set_symmetric_difference = unspecified; }
      }
              
    3. Add 25.7.? Set intersection view [range.set.intersection] as indicated:

      [25.7.?.1] Overview [range.set.intersection.overview]

      -1- set_intersection_view presents a view of set intersection between sorted ranges.

      -2- The name views::set_intersection denotes a customization point object [customization.point.object]. Let F be a subexpression, let FD be decay_t<decltype((F))>, and let Es... be a pack of subexpressions, the expression views::set_intersection(F, Es...) is expression-equivalent to set_intersection_view(ranges::less{}, F, Es...) if FD models range, and set_intersection_view(F, Es...) otherwise.

      -3- [Example 1:

      vector v1{1, 2, 2, 3,    4, 5, 6};
      vector v2{   2, 2, 3, 3,    5,    7};
      println("{}", views::set_intersection(v1, v2)); // prints [2 2 3 5]
      end example]

      [25.7.?.2] Class template set_intersection_view [range.set.intersection.view]

      namespace std::ranges {
      
      template<class F, class... Rs>
      constexpr bool pairwise-indirect-strict-weak-order = 
        (indirect_strict_weak_order<F, iterator_t<Rs>> && ...);           // exposition only
      template<class F, class R, class... Rs> requires(sizeof...(Rs) > 0)
      constexpr bool pairwise-indirect-strict-weak-order<F, R, Rs...> =   // exposition only
        (indirect_strict_weak_order<F, iterator_t<R>, iterator_t<Rs>> && ...) &&
        pairwise-indirect-strict-weak-order<F, Rs...>;
      
      template<class Comp, input_range... Views>
        requires (view<Views> && ...) && (sizeof...(Views) > 0) && is_object_v<Comp> &&
                  pairwise-indirect-strict-weak-order<Comp, Views...>
      class set_intersection_view : public view_interface<set_intersection_view<Comp, Views...>> {
          movable-box<Comp> comp_;      // exposition only
          tuple<Views...> views_;       // exposition only
      
          // [range.set.intersection.iterator], class set_intersection_view::iterator
          class iterator;          // exposition only
      
        public:
          set_intersection_view() = default;
          constexpr explicit set_intersection_view(Comp comp, Views... bases);
      
          constexpr Views...[0] base() const& requires copy_constructible<Views...[0]>
          { return std::get<0>(views_); }
          constexpr Views...[0] base() && { return std::move(std::get<0>(views_)); }
      
          constexpr iterator begin();
          constexpr default_sentinel_t end() const noexcept { return default_sentinel; }
      
          constexpr auto reserve_hint() requires (approximately_sized_range<Views> && ...);
          constexpr auto reserve_hint() const requires (approximately_sized_range<const Views> && ...);
        };
      
        template<class Comp, class... Rs>
          set_intersection_view(Comp, Rs&&...)
            -> set_intersection_view<Comp, views::all_t<Rs>...>;
      }
      
      constexpr explicit set_intersection_view(Comp comp, Views... bases);

      -1- Effects: Initializes comp_ with std::move(comp) and views_ with std::move(bases).... The behavior is erroneous if any range in the pack bases... is not sorted with respect to comp.

      constexpr iterator begin();

      -2- Returns: iterator(this, tuple-transform(ranges::begin, views_));

      -3- Remarks: In order to provide the amortized constant time complexity required by the range concept when set_intersection_view models forward_range, this function caches the result within the set_intersection_view for use on subsequent calls.

      constexpr auto reserve_hint() requires (approximately_sized_range<Views> && ...);
      constexpr auto reserve_hint() const requires (approximately_sized_range<const Views> && ...);

      -3- Effects: Equivalent to:

      return apply([](auto... sizes) {
        using CT = make-unsigned-like-t<common_type_t<decltype(sizes)...>>;
        return ranges::min({CT(sizes)...});
      }, tuple-transform(ranges::reserve_hint, views_));

      [25.7.?.2] Class set_intersection_view::iterator [range.set.intersection.iterator]

      namespace std::ranges {
      template<class Comp, input_range... Views>
        requires (view<Views> && ...) && (sizeof...(Views) > 0) && is_object_v<Comp> &&
                  pairwise-indirect-strict-weak-order<Comp, Views...>
      class set_intersection_view<Comp, Views...>::iterator {
          tuple<iterator_t<Views>...> current_;            // exposition only
          set_intersection_view* parent_ = nullptr;        // exposition only
      
          template<size_t N = 1>
          constexpr bool satisfy-next();                   // exposition only
          constexpr void satisfy();                        // exposition only
      
          constexpr explicit iterator(set_intersection_view* parent,
                                      tuple<iterator_t<Views>...> current);    // exposition only
      
        public:
          using iterator_category = see below;                        // not always present
          using iterator_concept =
            conditional_t<(forward_range<Views> && ...), forward_iterator_tag, input_iterator_tag>;
          using value_type = range_value_t<Views...[0]>;
          using difference_type = range_difference_t<Views...[0]>;
      
          iterator() = default;
      
          constexpr iterator_t<Views...[0]> base() && { return std::move(std::get<0>(current_)); }
          constexpr const iterator_t<Views...[0]>&
            base() const& noexcept { return std::get<0>(current_); }
      
          constexpr range_reference_t<Views...[0]> operator*() const { return *std::get<0>(current_); }
      
          constexpr iterator& operator++();
          constexpr void operator++(int);
          constexpr iterator operator++(int) requires (forward_range<Views> && ...) = default;
      
          friend constexpr bool operator==(const iterator& x, const iterator& y)
            requires (equality_comparable<iterator_t<Views>> && ...);
          friend constexpr bool operator==(const iterator& x, default_sentinel_t);
      
          friend constexpr range_rvalue_reference_t<Views...[0]> iter_move(const iterator& i)
            noexcept(noexcept(ranges::iter_move(std::get<0>(i.current_))));
        };
      }
      

      -1- The member typedef-name iterator_category is defined if and only if (forward_range<Views> && ...) is true. In that case, iterator::iterator_category is defined as follows: let Cs denote the pack of types iterator_traits<iterator_t<Views>>::iterator_category....

      1. (1.1.2) — If (derived_from<Cs, forward_iterator_tag> && ...) is true, iterator_category denotes forward_iterator_tag.

      2. (1.1.3) — Otherwise, iterator_category denotes input_iterator_tag.

      template<size_t N = 1>
      constexpr bool satisfy-next();

      -2- Effects: Equivalent to:

      if constexpr (N == sizeof...(Views))
        return true;
      else {
        auto& first = std::get<0>(current_);
        if (first == ranges::end(std::get<0>(parent_->views_)))
          return true;
        auto& other = std::get<N>(current_);
        while (true) {
          if (other == ranges::end(std::get<N>(parent_->views_)))
            return true;
          if (invoke(*parent_->comp_, *first, *other)) {
            ++first;
            return false;
          }
          if (invoke(*parent_->comp_, *other, *first))
            ++other;
          else
            break;
        }
        return satisfy-next<N + 1>();
      }
      
      constexpr void satisfy();

      -3- Effects: Equivalent to:

      while (!satisfy-next()) 
        ;
      
      [Note 1: set_intersection_view iterators use the satisfy function to find the next valid element in the first range.— end note]
      constexpr explicit iterator(set_intersection_view* parent, tuple<iterator_t<Views>...> current);

      -4- Effects: Initializes parent_ with parent and current_ with std::move(current); then calls satisfy().

      constexpr iterator& operator++();

      -5- Effects: Equivalent to:

      tuple-for-each([](auto& i) { ++i; }, current_);
      satisfy();
      return *this;
      
      constexpr void operator++(int);

      -6- Effects: Equivalent to ++*this.

      friend constexpr bool operator==(const iterator& x, const iterator& y)
        requires (equality_comparable<iterator_t<Views>> && ...);

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

      friend constexpr bool operator==(const iterator& x, default_sentinel_t);

      8- Returns: true if there exists an integer 0 ≤ i < sizeof...(Views) such that bool(std::get<i>(x.current_) == ranges::end(std::get<i>(x.parent_->views_))) is true. Otherwise, false.

      friend constexpr range_rvalue_reference_t<Views...[0]> iter_move(const iterator& i)
        noexcept(noexcept(ranges::iter_move(std::get<0>(i.current_))));

      -9- Effects: Equivalent to: return ranges::iter_move(std::get<0>(i.current_));

    4. Add 25.7.? Set union view [range.set.union] as indicated:

      [25.7.?.1] Overview [range.set.union.overview]

      -1- set_union_view presents a view of set union between sorted ranges.

      -2- The name views::set_union denotes a customization point object [customization.point.object]. Let F be a subexpression, let FD be decay_t<decltype((F))>, and let Es... be a pack of subexpressions, the expression views::set_union(F, Es...) is expression-equivalent to set_union_view(ranges::less{}, F, Es...) if FD models range, and set_union_view(F, Es...) otherwise.

      -3- [Example 1:

      vector v1{1, 2, 3, 4, 5};
      vector v2{      3, 4, 5, 6, 7};
      println("{}", views::set_union(v1, v2)); // prints [1 2 3 4 5 6 7]
      end example]

      [25.7.?.2] Class template set_union_view [range.set.union.view]

      namespace std::ranges {
      template<class Comp, input_range... Views>
        requires (view<Views> && ...) && (sizeof...(Views) > 0) && is_object_v<Comp> &&
                  pairwise-indirect-strict-weak-order<Comp, Views...> &&
                  concatable<Views...>
      class set_union_view : public view_interface<set_union_view<Comp, Views...>> {
          movable-box<Comp> comp_;      // exposition only
          tuple<Views...> views_;       // exposition only
      
          // [range.set.union.iterator], class template set_union_view::iterator
          template<bool> class iterator;          // exposition only
      
        public:
          set_union_view() = default;
          constexpr explicit set_union_view(Comp comp, Views... bases);
      
          constexpr iterator<false> begin();
          constexpr iterator<true> begin() const
            requires (range<const Views> && ...) &&
                     pairwise-indirect-strict-weak-order<const Comp, const Views...> &&
                     concatable<const Views...>;
      
          constexpr default_sentinel_t end() const noexcept { return default_sentinel; }
      
          constexpr auto reserve_hint() requires (approximately_sized_range<Views> && ...);
          constexpr auto reserve_hint() const requires (approximately_sized_range<const Views> && ...);
        };
      
        template<class Comp, class... Rs>
          set_union_view(Comp, Rs&&...)
            -> set_union_view<Comp, views::all_t<Rs>...>;
      }
      
      constexpr explicit set_union_view(Comp comp, Views... bases);

      -1- Effects: Initializes comp_ with std::move(comp) and views_ with std::move(bases).... The behavior is erroneous if any range in the pack bases... is not sorted with respect to comp.

      constexpr iterator<false> begin();

      -2- Returns: iterator<false>(this, tuple-transform(ranges::begin, views_));

      constexpr iterator<true> begin() const
        requires (range<const Views> && ...) &&
                  pairwise_indirect_strict_weak_order<const Comp, const Views...> &&
                  concatable<const Views...>;

      -3- Returns: iterator<true>(this, tuple-transform(ranges::begin, views_));

      constexpr auto reserve_hint() requires (approximately_sized_range<Views> && ...);
      constexpr auto reserve_hint() const requires (approximately_sized_range<const Views> && ...);

      -4- Effects: Equivalent to:

      return apply(
        [](auto... sizes) {
          using CT = make-unsigned-like-t<common_type_t<decltype(sizes)...>>;
          return (CT(sizes) + ...);
        }, tuple-transform(ranges::reserve_hint, views_));

      [25.7.?.2] Class template set_union_view::iterator [range.set.union.iterator]

      namespace std::ranges {
        template<class Comp, input_range... Views>
          requires (view<Views> && ...) && (sizeof...(Views) > 0) && is_object_v<Comp> &&
                    pairwise-indirect-strict-weak-order<Comp, Views...> &&
                    concatable<Views...>
        template<bool Const>
        class set_union_view<Comp, Views...>::iterator {
          static constexpr size_t inactive = -1;                      // exposition only
      
          tuple<iterator_t<maybe-const<Const, Views>>...> current_;   // exposition only
          maybe-const<Const, set_union_view>* parent_ = nullptr;      // exposition only
          size_t active_idx_ = inactive;                              // exposition only
      
          constexpr void satisfy();                                   // exposition only
      
          constexpr iterator(maybe-const<Const, set_union_view>* parent,        // exposition only
                             tuple<iterator_t<maybe-const<Const, Views>>...> current);
      
        public:
          using iterator_category = see below;                        // not always present
          using iterator_concept =
            conditional_t<(forward_range<maybe-const<Const, Views>> && ...),
                          forward_iterator_tag, input_iterator_tag>;
          using value_type = concat-value-t<maybe-const<Const, Views>...>;
          using difference_type =
            common_type_t<range_difference_t<maybe-const<Const, Views>>...>;
      
          iterator() = default;
      
          constexpr iterator(iterator<!Const> i)
            requires Const && (convertible_to<iterator_t<Views>, iterator_t<const Views>> && ...);
      
          constexpr concat-reference-t<maybe-const<Const, Views>...> operator*() const;
      
          constexpr iterator& operator++();
          constexpr void operator++(int);
          constexpr iterator operator++(int) requires (forward_range<maybe-const<Const, Views>> && ...) = default;
      
          friend constexpr bool operator==(const iterator& x, const iterator& y)
            requires (equality_comparable<iterator_t<maybe-const<Const, Views>>> && ...);
          friend constexpr bool operator==(const iterator& x, default_sentinel_t) noexcept;
      
          friend constexpr concat-rvalue-reference-t<maybe-const<Const, Views>...> iter_move(const iterator& i)
            noexcept(see below);
        };
      }

      -1- The member typedef-name iterator_category is defined if and only if all-forward<Const, Views ...> is modeled. In that case, iterator::iterator_category is defined as follows: let Cs denote the pack of types iterator_traits<iterator_t<maybe-const<Const, Views>>>::iterator_category....

      1. (1.1.2) — If (derived_from<Cs, forward_iterator_tag> && ...) is true, iterator_category denotes forward_iterator_tag.

      2. (1.1.3) — Otherwise, iterator_category denotes input_iterator_tag.

      constexpr void satisfy();

      -2- Effects:

      1. (2.1) — If there is no integer i in the range [0, sizeof...(Views)) such that bool(std::get<i>(current_) != ranges::end(std::get<i>(parent_->views_))) is true, assigns inactive to active_idx_.

      2. (2.2) — Otherwise, assigns to active_idx_ the least integer i in the range [0, sizeof...(Views)) such that bool(std::get<i>(current_) != ranges::end(std::get<i>(parent_->views_))) is true and, for every integer j in the range [0, sizeof...(Views)) such that j != i and bool(std::get<j>(current_) != ranges::end(std::get<j>(parent_->views_))) is true, bool(invoke(*parent_->comp_, *std::get<j>(current_), *std::get<i>(current_))) is false.

      [Note 1: set_union_view iterators use the satisfy function to find the next valid element in two ranges.— end note]
      constexpr iterator(maybe-const<Const, set_union_view>* parent,
                         tuple<iterator_t<maybe-const<Const, Views>>...> current);

      -3- Effects: Initializes parent_ with parent and current_ with std::move(current); then calls satisfy().

      constexpr iterator(iterator<!Const> i)
        requires Const && (convertible_to<iterator_t<Views>, iterator_t<const Views>> && ...);

      -4- Effects: Initializes parent_ with i.parent_, current_ with std::move(i.current_), and active_idx_ with i.active_idx_.

      constexpr concat-reference-t<maybe-const<Const, Views>...> operator*() const;

      -5- Preconditions: active_idx_ != inactive is true.

      -6- Effects: Let i be active_idx_. Equivalent to: return *std::get<i>(current_);

      constexpr iterator& operator++();

      -7- Preconditions: active_idx_ != inactive is true.

      -8- Effects: Let i be active_idx_. For every integer j in the range [0, sizeof...(Views)) such that j != i and bool(std::get<j>(current_) != ranges::end(std::get<j>(parent_->views_))) is true, increments std::get<j>(current_) if both bool(invoke(*parent_->comp_, *std::get<j>(current_), *std::get<i>(current_))) and bool(invoke(*parent_->comp_, *std::get<i>(current_), *std::get<j>(current_))) are false. Then increments std::get<i>(current_) and calls satisfy().

      -9- Returns: *this.

      constexpr void operator++(int);

      -10- Effects: Equivalent to ++*this.

      friend constexpr bool operator==(const iterator& x, const iterator& y)
        requires (equality_comparable<iterator_t<maybe-const<Const, Views>>> && ...);

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

      friend constexpr bool operator==(const iterator& i, default_sentinel_t) noexcept;

      -12- Effects: Equivalent to: return i.active_idx_ == inactive;

      friend constexpr concat-rvalue-reference-t<maybe-const<Const, Views>...> iter_move(const iterator& it)
            noexcept(see below);

      -13- Preconditions: it.active_idx_ != inactive is true.

      -14- Effects: Let i be active_idx_. Equivalent to: return ranges::iter_move(std::get<i>(it.current_));

      -15- Remarks: The exception specification is equivalent to:

      ((is_nothrow_invocable_v<decltype(ranges::iter_move),
                               const iterator_t<maybe-const<Const, Views>>&> &&
        is_nothrow_convertible_v<range_rvalue_reference_t<maybe-const<Const, Views>>,
                                 concat-rvalue-reference-t<maybe-const<Const, Views>...>>) &&
       ...)
    5. Add 25.7.? Set difference view [range.set.difference] as indicated:

      [25.7.?.1] Overview [range.set.difference.overview]

      -1- set_difference_view presents a view of set difference between two sorted ranges.

      -2- The name views::set_difference denotes a customization point object [customization.point.object]. Given subexpressions E, F and G, the expression views::set_difference(E, F) and views::set_difference(E, F, G) is expression-equivalent to set_difference_view(ranges::less{}, E, F) and set_difference_view(E, F, G), respectively.

      -3- [Example 1:

      vector v1{1, 2, 5, 5, 5,    9};
      vector v2{   2, 5,       7};
      println("{}", views::set_difference(v1, v2)); // prints [1, 5, 5, 9]
      end example]

      [25.7.?.2] Class template set_difference_view [range.set.difference.view]

      namespace std::ranges {
        template<class Comp, input_range V1, input_range V2>
          requires view<V1> && view<V2> && is_object_v<Comp> &&
          indirect_strict_weak_order<Comp, iterator_t<V1>, iterator_t<V2>>
        class set_difference_view : public view_interface<set_difference_view<Comp, V1, V2>> {
          movable-box<Comp> comp_;   // exposition only
          V1 base1_ = V1();          // exposition only
          V2 base2_ = V2();          // exposition only
       
          // [range.set.difference.iterator], class set_difference_view::iterator
          class iterator;          // exposition only
      
        public:
          set_difference_view() requires default_initializable<V1> && default_initializable<V2> &&
                                         default_initializable<Comp> = default;
      
          constexpr explicit set_difference_view(Comp comp, V1 base1, V2 base2);
      
          constexpr V1 base() const & requires copy_constructible<V1> { return base1_; }
          constexpr V1 base() && { return std::move(base1_); }
      
          constexpr iterator begin();
      
          constexpr default_sentinel_t end() const noexcept { return default_sentinel; }
      
          constexpr auto reserve_hint() requires approximately_sized_range<V1>
          { return ranges::reserve_hint(base1_); }
          constexpr auto reserve_hint() const requires approximately_sized_range<const V1>
          { return ranges::reserve_hint(base1_); }
        };
        
        template<class Comp, class R1, class R2>
          set_difference_view(Comp, R1&&, R2&&)
            -> set_difference_view<Comp, views::all_t<R1>, views::all_t<R2>>;
      }
      
      constexpr explicit set_difference_view(Comp comp, V1 base1, V2 base2);

      -1- Effects: Initializes comp_ with std::move(comp), base1_ with std::move(base1), and base2_ with std::move(base2). The behavior is erroneous if either base1_ or base2_ is not sorted with respect to comp.

      constexpr iterator begin();

      -2- Returns: {this, ranges::begin(base1_), ranges::begin(base2_)}.

      -3- Remarks: In order to provide the amortized constant time complexity required by the range concept when set_difference_view models forward_range, this function caches the result within the set_difference_view for use on subsequent calls.

      [25.7.?.2] Class set_difference_view::iterator [range.set.difference.iterator]

      namespace std::ranges {
         template<class Comp, input_range V1, input_range V2>
          requires view<V1> && view<V2> && is_object_v<Comp> &&
          indirect_strict_weak_order<Comp, iterator_t<V1>, iterator_t<V2>>
        class set_difference_view<Comp, V1, V2>>::iterator {
          iterator_t<V1> current1_ = iterator_t<V1>();    // exposition only
          iterator_t<V2> current2_ = iterator_t<V2>();    // exposition only
          set_difference_view* parent_ = nullptr;         // exposition only
      
          constexpr void satisfy();                       // exposition only
      
          constexpr iterator(set_difference_view* parent, 
                             iterator_t<V1> current1, iterator_t<V2> current2);    // exposition only
      
        public:
          using iterator_category = see below;                        // not always present
          using iterator_concept  =
            conditional_t<forward_range<V1> && forward_range<V2>, forward_iterator_tag, input_iterator_tag>;
          using value_type        = range_value_t<V1>;
          using difference_type   = range_difference_t<V1>;
      
          iterator()
            requires default_initializable<iterator_t<V1>> && default_initializable<iterator_t<V2>>
              = default;
      
          constexpr iterator_t<V1> base() && { return std::move(current1_); }
          constexpr const iterator_t<V1>& base() const & noexcept { return current1_; }
      
          constexpr decltype(auto) operator*() const { return *current1_; }
      
          constexpr iterator& operator++();
          constexpr void operator++(int);
          constexpr iterator operator++(int) requires forward_range<V1> && forward_range<V2> = default;
      
          friend constexpr bool operator==(const iterator& x, const iterator& y)
            requires equality_comparable<iterator_t<V1>>;
          friend constexpr bool operator==(const iterator& x, default_sentinel_t);
      
          friend constexpr decltype(auto) iter_move(const iterator& i)
            noexcept(noexcept(ranges::iter_move(i.current1_)));
        };
      }
      

      -1- The member typedef-name iterator_category is defined if and only if V1 and V2 model forward_range. In that case, iterator_category denotes forward_iterator_tag if qualified-id iterator_traits<iterator_t<V1>>::iterator_category and iterator_traits<iterator_t<V2>>::iterator_category denote a type that models derived_from<forward_iterator_tag>; otherwise it denotes input_iterator_tag.

      constexpr void satisfy();

      -2- Effects: Equivalent to:

      while (true) {
        if (current1_ == ranges::end(parent_->base1_))
          return;
        if (current2_ == ranges::end(parent_->base2_))
          return;
        if (invoke(*parent_->comp_, *current1_, *current2_))
          return;
        if (invoke(*parent_->comp_, *current2_, *current1_))
          ++current2_;
        else {
          ++current1_;
          ++current2_;
        }
      }
      
      [Note 1: set_difference_view iterators use the satisfy function to find the next valid element in the first range.— end note]
      constexpr iterator(set_difference_view* parent, iterator_t<V1> current1, iterator_t<V2> current2);

      -3- Effects: Initializes parent_ with parent, current1_ with std::move(current1) and current2_ with std::move(current2); then calls satisfy().

      constexpr iterator& operator++();

      -4- Effects: Equivalent to:

      ++current1_;
      satisfy();
      return *this;
      
      constexpr void operator++(int);

      -5- Effects: Equivalent to ++*this.

      friend constexpr bool operator==(const iterator& x, const iterator& y)
        requires equality_comparable<iterator_t<V1>>;

      -6- Effects: Equivalent to: return x.current1_ == y.current1_;

      friend constexpr bool operator==(const iterator& x, default_sentinel_t);

      -7- Effects: Equivalent to: return x.current1_ == x.end1_;

      friend constexpr decltype(auto) iter_move(const iterator& i)
        noexcept(noexcept(ranges::iter_move(i.current1_)));

      -8- Effects: Equivalent to: return ranges::iter_move(i.current1_);

    6. Add 25.7.? Set symmetric difference view [range.set.symmetric.difference] as indicated:

      [25.7.?.1] Overview [range.set.symmetric.difference.overview]

      -1- set_symmetric_difference_view presents a view of set difference between two sorted ranges.

      -2- The name views::set_symmetric_difference denotes a customization point object [customization.point.object]. Given subexpressions E, F and G, the expression views::set_symmetric_difference(E, F) and views::set_symmetric_difference(E, F, G) is expression-equivalent to set_symmetric_difference_view(ranges::less{}, E, F) and set_symmetric_difference_view(E, F, G), respectively.

      -3- [Example 1:

      vector v1{1, 3, 4,    6, 7, 9};
      vector v2{1,    4, 5, 6,    9};
      println("{}", views::set_symmetric_difference(v1, v2)); // prints [3 5 7]
      end example]

      [25.7.?.2] Class template set_symmetric_difference_view [range.set.symmetric.difference.view]

      namespace std::ranges {
        template<class Comp, input_range V1, input_range V2>
          requires view<V1> && view<V2> && is_object_v<Comp> &&
          indirect_strict_weak_order<Comp, iterator_t<V1>, iterator_t<V2>> &&
          concatable<V1, V2>
        class set_symmetric_difference_view : public view_interface<set_symmetric_difference_view<Comp, V1, V2>> {
          movable-box<Comp> comp_;   // exposition only
          V1 base1_ = V1();          // exposition only
          V2 base2_ = V2();          // exposition only
       
          // [range.set.symmetric.difference.iterator], class set_symmetric_difference_view::iterator
          class iterator;          // exposition only
      
        public:
          set_symmetric_difference_view() requires default_initializable<V1> && default_initializable<V2> &&
                                                   default_initializable<Comp> = default;
      
          constexpr explicit set_symmetric_difference_view(Comp comp, V1 base1, V2 base2);
      
          constexpr iterator begin();
      
          constexpr default_sentinel_t end() const noexcept { return default_sentinel; }
      
          constexpr auto reserve_hint() requires approximately_sized_range<V1> && 
                                                 approximately_sized_range<V2>
          { return ranges::reserve_hint(base1_) + ranges::reserve_hint(base2_); }
          constexpr auto reserve_hint() const requires approximately_sized_range<const V1> && 
                                                       approximately_sized_range<const V2>
          { return ranges::reserve_hint(base1_) + ranges::reserve_hint(base2_); }
        };
        
        template<class Comp, class R1, class R2>
          set_symmetric_difference_view(Comp, R1&&, R2&&)
            -> set_symmetric_difference_view<Comp, views::all_t<R1>, views::all_t<R2>>;
      }
      
      constexpr explicit set_symmetric_difference_view(Comp comp, V1 base1, V2 base2);

      -1- Effects: Initializes comp_ with std::move(comp), base1_ with std::move(base1), and base2_ with std::move(base2). The behavior is erroneous if either base1_ or base2_ is not sorted with respect to comp.

      constexpr iterator begin();

      -2- Returns: {this, ranges::begin(base1_), ranges::begin(base2_)}.

      -3- Remarks: In order to provide the amortized constant time complexity required by the range concept when set_symmetric_difference_view models forward_range, this function caches the result within the set_symmetric_difference_view for use on subsequent calls.

      [25.7.?.2] Class set_symmetric_difference_view::iterator [range.set.symmetric.difference.iterator]

      namespace std::ranges {
         template<class Comp, input_range V1, input_range V2>
          requires view<V1> && view<V2> && is_object_v<Comp> &&
          indirect_strict_weak_order<Comp, iterator_t<V1>, iterator_t<V2>>
        class set_symmetric_difference_view<Comp, V1, V2>>::iterator {
          static constexpr size_t inactive = -1;                      // exposition only
      
          iterator_t<V1> current1_ = iterator_t<V1>();                // exposition only
          iterator_t<V2> current2_ = iterator_t<V2>();                // exposition only
          set_symmetric_difference_view* parent_ = nullptr;           // exposition only
          size_t active_idx_ = inactive;                              // exposition only
      
          constexpr void satisfy();                       // exposition only
      
          constexpr iterator(set_symmetric_difference_view* parent, 
                             iterator_t<V1> current1, iterator_t<V2> current2);    // exposition only
      
        public:
          using iterator_category = see below;                        // not always present
          using iterator_concept  =
            conditional_t<forward_range<V1> && forward_range<V2>, forward_iterator_tag, input_iterator_tag>;
          using value_type        = range_value_t<V1>;
          using difference_type   = range_difference_t<V1>;
      
          iterator()
            requires default_initializable<iterator_t<V1>> && default_initializable<iterator_t<V2>>
              = default;
      
          constexpr concat-reference-t<V1, V2> operator*() const;
      
          constexpr iterator& operator++();
          constexpr void operator++(int);
          constexpr iterator operator++(int) requires forward_range<V1> && forward_range<V2> = default;
      
          friend constexpr bool operator==(const iterator& x, const iterator& y)
            requires equality_comparable<iterator_t<V1>> && equality_comparable<iterator_t<V2>>;
          friend constexpr bool operator==(const iterator& x, default_sentinel_t) noexcept;
      
          friend constexpr concat-rvalue-reference-t<V1, V2> iter_move(const iterator& i) noexcept(see below);
        };
      }
      

      -1- The member typedef-name iterator_category is defined if and only if V1 and V2 model forward_range. In that case, iterator_category denotes forward_iterator_tag if qualified-id iterator_traits<iterator_t<V1>>::iterator_category and iterator_traits<iterator_t<V2>>::iterator_category denote a type that models derived_from<forward_iterator_tag>; otherwise it denotes input_iterator_tag.

      constexpr void satisfy();

      -2- Effects: Equivalent to:

      
      while (true) {
        if (current1_ == ranges::end(parent_->base1)) {
          if (current2_ == ranges::end(parent_->base2))
            active_idx_ = inactive;
          else
            active_idx_ = 1;
          return;
        }
        if (current2_ == ranges::end(parent_->base2))  {
          active_idx_ = 0;
          return;
        }
        if (invoke(*parent_->comp_, *current1_, *current2_)) {
          active_idx_ = 0;
          return;
        }
        if (invoke(*parent_->comp_, *current2_, *current1_)) {
          active_idx_ = 1;
          return;
        }
        ++current1_;
        ++current2_;
      }
      
      [Note 1: set_symmetric_difference_view iterators use the satisfy function to find the next valid element in two ranges.— end note]
      constexpr iterator(set_symmetric_difference_view* parent, iterator_t<V1> current1, iterator_t<V2> current2);

      -3- Effects: Initializes parent_ with parent,current1_ with std::move(current1), and current2_ with std::move(current2); then calls satisfy().

      constexpr concat-reference-t<V1, V2> operator*() const;

      -4- Preconditions: active_idx_ != inactive is true.

      -5- Effects: Equivalent to:

      
      if (active_idx_ == 0)
        return *current1_;
      return *current2_;
      
      constexpr iterator& operator++();

      -6- Preconditions: active_idx_ != inactive is true.

      -7- Effects: Equivalent to:

      if (active_idx_ == 0)
        ++current1_;
      else
        ++current2_;
      satisfy();
      return *this;
      
      constexpr void operator++(int);

      -8- Effects: Equivalent to ++*this.

      friend constexpr bool operator==(const iterator& x, const iterator& y)
        requires equality_comparable<iterator_t<V1>> && equality_comparable<iterator_t<V2>>;

      -9- Effects: Equivalent to: return x.current1_ == y.current1_ && x.current2_ == y.current2_;

      friend constexpr bool operator==(const iterator& i, default_sentinel_t) noexcept;

      -10- Effects: Equivalent to: return i.active_idx_ == inactive;

      friend constexpr concat-rvalue-reference-t<V1, V2> iter_move(const iterator& i) noexcept(see below);

      -11- Preconditions: i.active_idx_ != inactive is true.

      -12- Effects: Equivalent to:

      if (i.active_idx_ == 0)
        return ranges::iter_move(i.current1_);
      return ranges::iter_move(i.current2_);

      -13- Remarks: The exception specification is equivalent to:

      noexcept(ranges::iter_move(i.current1_)) &&
      noexcept(ranges::iter_move(i.current2_)) &&
      is_nothrow_convertible_v<range_rvalue_reference_t<V1>, concat-rvalue-reference-t<V1, V2>> && 
      is_nothrow_convertible_v<range_rvalue_reference_t<V2>, concat-rvalue-reference-t<V1, V2>>

References

[P2760R1]
Barry Revzin. A Plan for C++26 Ranges. URL: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2023/p2760r1.html
[range/v3]
Eric Niebler. views::set_operations implementation. URL: https://github.com/ericniebler/range-v3/blob/master/include/range/v3/view/set_algorithm.hpp