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

- Abstract
  - Revision history
  - Discussion
  - Design
  - Implementation experience
  - Proposed change
  - References

## Abstract

This paper proposes adding `views::cycle`, a Tier 1 range adaptor as described in [P2760](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2023/p2760r1.html), to enhance the C++29 Ranges library by enabling infinite repetition of a range's elements.

## Revision history

### R0

Initial revision.

### R1

1. Provide a bound version.
2. Fix the calculation of cycle count according to [user reports](https://github.com/ericniebler/range-v3/issues/1856).
3. Provide `iter_swap` specialization.

## Discussion

There is currently no standard range adaptor in C++ that allows repeating a range endlessly. Although similar behavior can be approximated via `views::repeat(r) | views::join` or a custom `generator`, such constructs are limited to forward ranges and often introduce additional complexity and boilerplate. Additionally, they lack the semantic clarity and composability offered by a dedicated adaptor.

The ability to cycle through elements infinitely is a common requirement in many domains: circular buffers, animations, event loops, and more. This functionality exists natively in other modern languages (e.g., Python's `itertools.cycle`, Rust's `.cycle()`), highlighting a gap in C++'s otherwise powerful Ranges library.

We propose introducing `views::cycle` as a simple, intuitive, and efficient adaptor for producing infinite, multi-pass ranges that cycle through the original range. This fills a notable gap in the existing standard and improves parity with other languages while supporting both eager and lazy range pipelines:

```
  for (auto&& song : playlist | views::cycle | views::take(100)) {
    play(song);
  }
```

## Design

### Class signature

Since `views::cycle` repeats the original range, the minimum requirement is `forward_range` to ensure multi-pass. The proposed signature of `cycle_view` is:

```
  template<view V>
    requires forward_range<V>
  class cycle_view : public view_interface<cycle_view<V>> {
    // ...
  };
  
```

### Handling of Empty Ranges

range/v3's cycle view (which is named `cycled_view`) requires the original range to be [non-empty](https://github.com/ericniebler/range-v3/blob/ca1388fb9da8e69314dda222dc7b139ca84e092f/include/range/v3/view/cycle.hpp#L198C9-L202C10):

```
  explicit cycled_view(Rng rng)
    : rng_(std::move(rng))
  {
      RANGES_EXPECT(!ranges::empty(rng_));
  }
```

It always expects a non-empty range and produces an infinite range. This requires the user to ensure the input range is non-empty, or else the behavior is undefined:

```
  auto e = ranges::views::empty<int>;
  auto c1 = e | ranges::views::cycle;   // UB

  vector<int> v;
  auto c2 = v | ranges::views::cycle;   // also UB
```

Unlike range-v3's `views::cycle`, the proposed `views::cycle` supports empty ranges. This is consistent with other standard range adaptors that naturally handle empty input by producing empty views. Supporting empty ranges avoids undefined behavior in common scenarios, such as default-initializing a `cycle_view`:

```
  auto c = vector{42} | views::cycle;
  decltype(c) c2{};     // Default construction is now well-formed
```

This improves composability by aligning with the standard library's treatment of empty ranges, yielding intuitive and consistent behavior. Consequently, since a `cycle_view` can now be empty, its `end()` cannot be `unreachable_sentinel` as in range-v3. Instead, we use `default_sentinel`, with the following semantics:

- If the base range is empty, `begin() == end()` and the `cycle_view` is empty.
- If the base range is non-empty, iteration proceeds infinitely, and `end()` is never reached.

This approach preserves correctness for both empty and infinite cases without introducing special-case logic.

Supporting empty ranges means we must handle `operator+=` more carefully. Since the arithmetic relies on the underlying range's size to calculate offsets and cycle counts, a division by zero will occur if the range is empty. To prevent this, we include an early return in `operator+=` when the distance is zero. This ensures that expressions like `views::cycle(views::empty<int>).begin() += 0` remain safe and well-defined, effectively treating any advancement on an empty cycle as a no-op.

### End Iterator Is Not Cached

For bidirectional operations such as `operator--()`, when we find that the current iterator has reached the beginning of the original range, we need to move the current iterator back to the last element of the original range:

```
  if (current_ == ranges::begin(base_)) {
    current_ = end-iterator-of-base_; // Repositioning
    // ...
  }
  --current_;
```

However, getting the end iterator of the original range may not be constant time unless the original range is `common_range` or models both `sized_range` and `random_access_range`; in such case, we can extract them as we do in `*cartesian-common-arg-end*`:

```
  template<cartesian-product-common-arg R>
  constexpr auto cartesian-common-arg-end(R& r) {       // exposition only
    if constexpr (common_range<R>) {
      return ranges::end(r);
    } else {
      return ranges::begin(r) + ranges::distance(r);
    }
  }
```

However, it is worth noting that range/v3 takes a more aggressive approach, providing bidirectional operations as long as the underlying range models `bidirectional_range`, and providing random-access operations as long as the underlying range models `random_access_range`.

For unsized ranges, random-access operation requires knowing the actual size to locate the correct position. In this case, range/v3's `cycled_view` will *eagerly* calculate the end iterator of the original range and [cache](https://github.com/ericniebler/range-v3/blob/ca1388fb9da8e69314dda222dc7b139ca84e092f/include/range/v3/view/cycle.hpp#L79-L87) it internally, so that it can be used to compute the size next time:

```
  const char* s = /* */;
  ranges::subrange rng(s,null_sentinel{});
  static_assert(!ranges::sized_range<decltype(rng)>);

  auto cycled = rng | ranges::views::cycle;
  auto it = cycled.begin() + 42;   // O(n) time to get the rng's end iterator for computing size
```

Similarly, for bidirectional operations, range-v3 will also eagerly cache the end iterator if the underlying range is not `common_range`.

Unlike range/v3, we deliberately avoid this optimization to prevent hidden costs and semantic surprises. Instead, we propose to restrict such operations only when the underlying range natively supports them, that is, when it is a `random_access_range` and `sized_range` for random-access, or a `bidirectional_range` and `common_range` for bidirectional support.

This avoids hidden O(n) complexity, preserves lazy evaluation, reduces internal state, and eliminates the need for caching, consistent with the design philosophy of other standard views.

### difference_type Considerations

To support operations such as iterator difference or positioning, `cycle_view::*iterator*` needs to track how many full passes have been made through the base range. This is typically implemented by maintaining a counter `n`, which is incremented each time the iterator loops back to the beginning of the base range.

Although `n` can be represented using the `difference_type` of the underlying range, since the number of cycles itself only needs to count a finite number of iterations, for `difference_type` of `cycle_view::*iterator*`, the original `difference_type` may not be a good choice.

The reason is that the logical distance between two `cycle_view::*iterator*`s may become much larger than any value that can be stored in the original `difference_type`; in such cases, subtracting them will result in overflow which leads to UB.

Noted that range/v3 chooses `std::intmax_t`, and we propose using an integral-class type to represent the `difference_type` of `cycle_view::*iterator*`, that is, an implementation-defined integer type that is sufficiently wide to accommodate the expected range of iteration.

### Support for Bounded Cycle

Although range-v3 only provides an infinite cycle, we propose adding an optional count parameter: `views::cycle(R, N)`.

Bounded cycles are a common requirement. While Ruby provides native `cycle(n)`, other ecosystems typically rely on composing infinite cycles with truncation. Native support in C++ aligns with the design of `views::repeat(x, N)` and is strictly superior to the `views::repeat(R, N) | views::join` workaround: it exposes a `sized_range` interface, reduces view pipeline complexity, and improves compilation performance.

### Not borrow range

`cycle_view` cannot be a `borrowed_range`, because its iterators internally store a pointer to the `cycle_view` in order to access the stored underlying range.

Specifically, the iterator relies on calling `ranges::end`/`ranges::begin` on underlying range to determine whether it has reached the end/begin of a cycle. As such, `cycle_view` cannot be a `borrowed_range`, because the validity of its iterators depends on the lifetime of the view object.

### Common range

A bounded `cycle_view` satisfies the `common_range` by defining its terminal state using an iterator rather than a sentinel. For a range cycled `*N*` times, the end of the range is defined as the moment the iteration completes exactly `*N*` cycles, which corresponds to a state of `n_ = *N*` and an internal iterator pointing to `ranges::begin(*base_*)`.

This allows every bounded `cycle_view` to trivially model `common_range`. This architecture eliminates the need for a dedicated sentinel type and provides seamless interoperability with standard algorithms and legacy APIs that require [`begin, end`) iterator pairs.

## Implementation experience

The author implemented `views::cycle` based on libstdc++, see [here](https://godbolt.org/z/o5s4jM38h).

## Proposed change

This wording is relative to [latest working draft](https://eel.is/c++draft/).

## References

[P2760R1]

Barry Revzin. A Plan for C++26 Ranges. URL:

https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2023/p2760r1.html
