---
title: "views::unique"
document: P4291R0
date: 2026-07-01
audience: LEWG, SG9 (Ranges)
reply-to:
  - "Hewill Kang <hewillk@gmail.com>"
---

- Abstract
- Revision history
  - R0
- Motivation
- Design
  - Forward range requirement
  - Custom equivalence relation
  - Bidirectional support
  - Common range behavior
  - Const propagation
  - Borrowed range support
- Implementation experience
- Wording
- References

## Abstract

This paper proposes a range adaptor `views::unique` that filters out consecutive equivalent elements from a range, leaving only one element from each group of consecutive equivalent elements. This adaptor complements the existing `std::unique` algorithm by providing a composable, lazy, and allocation-free view for a common filtering operation.

The adaptor fills a notable gap in the Ranges library by enabling functional composition of uniqueness filtering with other range operations, consistent with the design philosophy of range adaptors.

## Revision history

### R0

Initial revision.

## Motivation

The standard library provides `std::unique` algorithm for removing consecutive equivalent elements. However, like most standard algorithms, it requires in-place modification and returns an iterator to the new end of the range. This approach modifies the underlying range, which may not be desired, and requires eager evaluation that forces all elements to be processed. Furthermore, it cannot be easily composed with other range operations, and the returned range must be manually resized or sliced before use.

A range adaptor approach provides a fundamentally different experience, enabling lazy evaluation where elements are processed on-demand, non-destructive filtering that does not modify the underlying range, and seamless composition with other adaptors through the pipe operator. Consider the contrast:

```cpp
  /* algorithm approach */
  std::vector<int> v = {1, 1, 2, 2, 2, 3, 1, 1};
  auto last = std::unique(v.begin(), v.end());
  v.erase(last, v.end());  // Modifies original range

  /* range adaptor approach */
  auto v = std::vector{1, 1, 2, 2, 2, 3, 1, 1};
  auto uniq = v | views::unique;  // Lazy, non-modifying, composable
```

## Design

### Forward range requirement

The `unique_view` requires its underlying range to be a `forward_range`. This requirement exists because comparing consecutive elements for equivalence requires maintaining a reference to the current element while advancing. An input range cannot support this since once we advance, we lose access to the previous element. Forward iterators also provide the multi-pass guarantee, allowing multiple iterations over the view.

### Custom equivalence relation

The predicate must satisfy `indirect_equivalence_relation<iterator_t<V>>`, defaulting to `ranges::equal_to`. Callables like function pointers, lambda expressions, and member function pointers (via `std::invoke`) are supported, enabling filtering based on custom logic:

```cpp
  auto v = std::vector{1, 1, 2, 2, 3, 3};
  auto uniq = v | views::unique;
  // uniq yields: 1, 2, 3

  auto words = std::vector{std::string{"cat"}, std::string{"CAT"}, std::string{"dog"}};
  auto case_insensitive = words | views::unique([](std::string_view a, std::string_view b) {
    return std::ranges::equal(a, b, [](char x, char y) {
      return std::tolower(static_cast<unsigned char>(x)) ==
             std::tolower(static_cast<unsigned char>(y));
    });
  });
  // case_insensitive yields: "cat", "dog"

  struct Record { int key; std::string name; };
  auto records = std::vector{Record{1, "a"}, Record{1, "b"}, Record{2, "c"}};
  auto by_key = records | views::unique([](auto&& a, auto&& b) {
    return a.key == b.key;
  });
  // by_key yields: {1, "a"}, {2, "c"}
```

### Bidirectional support

Supporting `operator--` is quite doable. The idea is simple: when we move backward, we do not just step one position and stop. We need to find the previous visible element in the view.

Noted that when we move backward, we need to land on the first element of that run, not on one of the skipped duplicates. That is the element that should be exposed as the previous visible value.

It should be noted that if `views::unique` supports `operator--`, combining it with an rvalue pipeline and backward traversal inevitably exposes the exact same semantic breakdown discussed in [*Filter View Extensions for Safer Use*](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2026/p3725r3.pdf), as in the following example:

```cpp
vector<string> coll{"hello", "hello"};
auto result = coll | views::unique
                   | views::reverse
                   | views::as_rvalue
                   | ranges::to<std::vector>();
println("{}", result); // Expected: ["hello"], Actual: ["hello", ""]
```

This inherent tension between a 'destructive move' and 'bidirectional traversal requiring repeated look-backs at element values' highlights a known design challenge in the Ranges.

Given that supporting `operator--` still holds some practical value and intuitive utility in conventional lvalue scenarios (e.g., purely traversing `std::vector<int> | unique | reverse`), the author has chosen to retain support for `bidirectional_range` for the time being.

Nonetheless, whether this design should remain as-is or follow the direction of certain filtering views by strictly limiting `views::unique` to a `forward_range` demands explicit consideration and direction from LEWG.

### Common range behavior

When the underlying range is a `common_range`, the `end()` function returns an iterator. Otherwise, it returns a sentinel that only stores the underlying sentinel:

```cpp
  if constexpr (common_range<V>)
    return iterator<Const>(this, ranges::end(base_));
  else
    return sentinel<Const>(ranges::end(base_));
```

### Const propagation

The adaptor provides separate const and non-const overloads of `begin()`, allowing users to obtain const or non-const iterators as appropriate.

### Borrowed range support

In principle, `unique_view` could be `borrowed_range` if the underlying view `V` is a `borrowed_range` and the equivalence relation `Pred` is a stateless, "tiny" object (such as `ranges::equal_to`) that can be trivially reconstructed without increasing footprint.

However, the iterator of `unique_view` still needs to know where the boundary of the underlying sequence is to safely skip consecutive duplicates during increment operations. This implies that the iterators must store the `end` sentinel of the underlying range to have access to it.

At present, the author has no immediate intention to pursue `borrowed_range` specialization for `unique_view`. Nevertheless, the author remain neutral on this matter and welcome further feedback from the committee if such support is deemed desirable.

## Implementation experience

The implementation of `views::unique` has been tested and validated using the [Compiler Explorer](https://godbolt.org/z/MeTTf5bvq).

## Wording

This wording is relative to [N5046](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2026/n5046.pdf).

Add a new feature-test macro to 17.3.2 [[version.syn]](https://eel.is/c++draft/version.syn):

> ```cpp
> #define __cpp_lib_ranges_unique 2026XXL // freestanding, also in <ranges>
> ```

Modify 25.2 [[ranges.syn]](https://eel.is/c++draft/ranges.syn), Header `<ranges>` synopsis, as indicated:

> ```cpp
> namespace std::ranges {
>   // [range.unique], unique view
>   template<forward_range V,
>            indirect_equivalence_relation<iterator_t<V>> Pred = ranges::equal_to>
>     requires view<V> && is_object_v<Pred>
>   class unique_view;
> 
>   namespace views { inline constexpr unspecified unique = unspecified; }
> }
> ```

Add **26.7.? Unique view [range.unique]** after 25.7.35 [[range.as.input]](https://eel.is/c++draft/range.as.input) as indicated:

[26.7.?.1] Overview [range.unique.overview]

-1- `unique_view` presents a view of an underlying sequence with consecutive equivalent elements removed, where equivalence is determined by a predicate.

-2- The name `views::unique` denotes a range adaptor object (25.7.2 [[range.adaptor.object]](https://eel.is/c++draft/range.adaptor.object)). Given subexpressions `E` and F, the expression `views::unique(E)` and `views::unique(E, F)` is expression-equivalent to `unique_view(E)` and `unique_view(E, F)`, respectively.

-3- [*Example 1*:

> ```cpp
> vector v = {1, 1, 2, 2, 2, 3, 1, 1};
> auto uniq = v | views::unique;
> for (int i : uniq)
>   cout << i <<; ' ';   // prints 1 2 3 1
> ```
> 
> —
> 
> end example]

[26.7.?.2] Class template `unique_view` [range.unique.view]

> ```cpp
> namespace std::ranges {
>   template<forward_range V,
>            indirect_equivalence_relation<iterator_t<V>> Pred = ranges::equal_to>
>     requires view<V> && is_object_v<Pred>
>   class unique_view : public view_interface<unique_view<V, Pred>> {
>     V base_ = V();                      // exposition only
>     movable-box<Pred> pred_;            // exposition only
> 
>     // [range.unique.iterator], class template unique_view::iterator
>     template<bool Const>
>     class iterator;                     // exposition only
> 
>     // [range.unique.sentinel], class template unique_view::sentinel
>     template<bool Const>
>     class sentinel;                     // exposition only
> 
>   public:
>     unique_view() requires default_initializable<V> && default_initializable<Pred> = default;
>     constexpr explicit unique_view(V base, Pred pred = {});
> 
>     constexpr V base() const& requires copy_constructible<V> { return base_; }
>     constexpr V base() && { return std::move(base_); }
> 
>     constexpr const Pred& pred() const;
> 
>     constexpr iterator<false> begin() { return iterator<false>(this, ranges::begin(base_)); }
>     constexpr iterator<true> begin() const
>       requires forward_range<const V> &&
>                indirect_equivalence_relation<const Pred, iterator_t<const V>> {
>       return iterator<true>(this, ranges::begin(base_));
>     }
> 
>     constexpr auto end() { 
>       if constexpr (common_range<V>)
>         return iterator<false>(this, ranges::end(base_));
>       else
>         return sentinel<false>(ranges::end(base_)); 
>     }
> 
>     constexpr auto end() const
>       requires forward_range<const V> &&
>                indirect_equivalence_relation<const Pred, iterator_t<const V>> {
>       if constexpr (common_range<const V>)
>         return iterator<true>(this, ranges::end(base_));
>       else
>         return sentinel<true>(ranges::end(base_));
>     }
>   };
> 
>   template<class R, class Pred>
>   unique_view(R&&, Pred) -> unique_view<views::all_t<R>, Pred>;
> }
> ```

> ```cpp
> constexpr explicit unique_view(V base, Pred pred = {});
> ```

> -1- Effects:
> 
> Initializes
> 
> base_
> 
> with
> 
> std::move(base)
> 
> and
> 
> pred_
> 
> with
> 
> std::move(pred)
> 
> .

> ```cpp
> constexpr const Pred& pred() const;
> ```

> *-2- Preconditions*: `pred_.has_value()` is `true`.
> 
> *-3- Effects*: Equivalent to: `return *pred_;`

[26.7.?.3] Class template `unique_view::*iterator*` [range.unique.iterator]

> ```cpp
> namespace std::ranges {
>   template<forward_range V,
>            indirect_equivalence_relation<iterator_t<V>> Pred = ranges::equal_to>
>     requires view<V> && is_object_v<Pred>
>   template<bool Const>
>   class unique_view<V, Pred>::iterator {
>     using Parent = maybe-const<Const, unique_view>;                  // exposition only
>     using Base = maybe-const<Const, V>;                              // exposition only
>     Parent* parent_ = nullptr;                                       // exposition only
>     iterator_t<Base> current_ = iterator_t<Base>();                  // exposition only
>     constexpr iterator(Parent* parent, iterator_t<Base> current);    // exposition only
> 
>   public:
>     using iterator_concept = 
>       conditional_t<bidirectional_range<Base>, bidirectional_iterator_tag, forward_iterator_tag>;
>     using iterator_category = see below;
>     using value_type = range_value_t<Base>;
>     using difference_type = range_difference_t<Base>;
> 
>     iterator() = default;
>     constexpr iterator(iterator<!Const> i)
>       requires Const && convertible_to<iterator_t<V>, iterator_t<Base>>;
> 
>     constexpr iterator_t<Base> base() const;
> 
>     constexpr range_reference_t<Base> operator*() const;
>     constexpr iterator_t<Base> operator->() const requires has-arrow<iterator_t<Base>>;
> 
>     constexpr iterator& operator++();
>     constexpr iterator operator++(int) = default;
> 
>     constexpr iterator& operator--() requires bidirectional_range<Base>;
>     constexpr iterator operator--(int) requires bidirectional_range<Base> = default;
> 
>     friend constexpr bool operator==(const iterator& x, const iterator& y);
> 
>     friend constexpr range_rvalue_reference_t<Base> iter_move(const iterator& i)
>       noexcept(noexcept(ranges::iter_move(i.current_)));
> 
>     friend constexpr void iter_swap(const iterator& x, const iterator& y)
>       noexcept(noexcept(ranges::iter_swap(x.current_, y.current_)))
>       requires indirectly_swappable<iterator_t<Base>>;
>   };
> }
> ```

> 1. (1.1) — Let `C` denote the type `iterator_traits<iterator_t<Base>>::iterator_category`.
> 2. (1.2) — If `C` models `derived_from<bidirectional_iterator_tag>`, then `iterator_category` denotes `bidirectional_iterator_tag`.
> 3. (1.3) — Otherwise, if `C` models `derived_from<forward_iterator_tag>`, then `iterator_category` denotes `forward_iterator_tag`.
> 4. (1.4) — Otherwise, `iterator_category` denotes `input_iterator_tag`.
> 
> ```cpp
> constexpr iterator(Parent* parent, iterator_t<Base> current);
> ```
> 
> > -2- Effects:
> > 
> > Initializes
> > 
> > parent_
> > 
> > with
> > 
> > parent
> > 
> > and
> > 
> > current_
> > 
> > with
> > 
> > current
> > 
> > .
> 
> ```cpp
> constexpr iterator(iterator<!Const> i)
>       requires Const && convertible_to<iterator_t<V>, iterator_t<Base>>;
> ```
> 
> > -3- Effects:
> > 
> > Initializes
> > 
> > parent_
> > 
> > with
> > 
> > i.parent_
> > 
> > and
> > 
> > current_
> > 
> > with
> > 
> > std::move(i.current_)
> > 
> > .
> 
> ```cpp
> constexpr iterator_t<Base> base() const;
> ```
> 
> > -4- Effects
> > 
> > : Equivalent to:
> > 
> > return current_;
> 
> ```cpp
> constexpr range_reference_t<Base> operator*() const;
> ```
> 
> > -5- Effects
> > 
> > : Equivalent to:
> > 
> > return *current_;
> 
> ```cpp
> constexpr iterator_t<Base> operator->() const requires has-arrow<iterator_t<Base>>;
> ```
> 
> > -6- Effects
> > 
> > : Equivalent to:
> > 
> > return current_;
> 
> ```cpp
> constexpr iterator& operator++();
> ```
> 
> > -7- Effects:
> > 
> > Equivalent to:
> > 
> > ```cpp
> > auto&& deref = *current_;
> > while (++current_ != ranges::end(parent_->base_))
> >   if (!std::invoke(*parent_->pred_, std::forward<decltype(deref)>(deref), *current_))
> >     break;
> > return *this;
> > ```
> 
> ```cpp
> constexpr iterator& operator--() requires bidirectional_range<Base>;
> ```
> 
> > -8- Effects:
> > 
> > Equivalent to:
> > 
> > ```cpp
> > auto&& deref = *--current_;
> > while (current_ != std::ranges::begin(parent_->base_)) {
> >   auto prev = ranges::prev(current_);
> >   if (!std::invoke(*parent_->pred_, *prev, std::forward<decltype(deref)>(deref)))
> >     break;
> >   current_ = prev;
> > }
> > return *this;
> > ```
> 
> ```cpp
> friend constexpr bool operator==(const iterator& x, const iterator& y);
> ```
> 
> > -9- Effects:
> > 
> > Equivalent to:
> > 
> > return x.current_ == y.current_;
> 
> ```cpp
> friend constexpr range_rvalue_reference_t<Base> iter_move(const iterator& i)
>   noexcept(noexcept(ranges::iter_move(i.current_)));
> ```
> 
> > -10- Effects:
> > 
> > Equivalent to:
> > 
> > return ranges::iter_move(i.current_);
> 
> ```cpp
> friend constexpr void iter_swap(const iterator& x, const iterator& y)
>   noexcept(noexcept(ranges::iter_swap(x.current_, y.current_)))
>   requires indirectly_swappable<iterator_t<Base>>;
> ```
> 
> > -11- Effects:
> > 
> > Equivalent to
> > 
> > ranges::iter_swap(x.current_, y.current_);
> 
> [26.7.?.3] Class template `unique_view::*sentinel*` [range.unique.sentinel]
> 
> ```cpp
> namespace std::ranges {
>   template<forward_range V,
>            indirect_equivalence_relation<iterator_t<V>> Pred = ranges::equal_to>
>     requires view<V> && is_object_v<Pred>
>   template<bool Const>
>   class unique_view<V, Pred>::sentinel {
>     using Base = maybe-const<Const, V>;                   // exposition only
>     sentinel_t<Base> end_ = sentinel_t<Base>();           // exposition only
>     constexpr explicit sentinel(sentinel_t<Base> end);    // exposition only
> 
>   public:
>     sentinel() = default;
>     constexpr sentinel(sentinel<!Const> other)
>       requires Const && convertible_to<sentinel_t<V>, sentinel_t<Base>>;
> 
>     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);
>   };
> }
> ```
> 
> ```cpp
> constexpr explicit sentinel(sentinel_t<Base> end);
> ```
> 
> > -1- Effects:
> > 
> > Initializes
> > 
> > end_
> > 
> > with
> > 
> > end
> > 
> > .
> 
> ```cpp
> constexpr sentinel(sentinel<!Const> other)
> ```
> 
> > -2- Effects:
> > 
> > Initializes
> > 
> > end_
> > 
> > with
> > 
> > std::move(other.end_)
> > 
> > .
> 
> ```cpp
> constexpr sentinel_t<Base> base() const;
> ```
> 
> > -3- Effects
> > 
> > : Equivalent to:
> > 
> > return end_;
> 
> ```cpp
> 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_;
> 
> ## References
> 
> - [P2760R1] Barry Revzin. A Plan for C++26 Ranges. URL: [https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2023/p2760r1.html](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2023/p2760r1.html)
> - [range/v3] Eric Niebler. Range library v3. URL: [https://github.com/ericniebler/range-v3](https://github.com/ericniebler/range-v3)
