Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 20 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,16 @@
This library explores an opinionated re-design of C++ Range Adaptors, commonly referred to as "views". It tries to reduce complexity in the conceptual space and provide a better, more consistent user experience.
At the same time, the usage patterns and naming remain close enough to the standard library to be used almost as a drop-in replacement.


## 🪞 Differences and similarities to std::ranges

```cpp
auto adapted_range = original_range | range_adaptor_object | range_adaptor_object2;
auto range_adaptor = underlying_range | range_adaptor_object | range_adaptor_object2;
```

The general pattern for creating adapted ranges is the same in our library and in the standard library.
However, we aim to establish clearer rules for what you can and cannot do with the `adapted_range`.
To achieve this, we are sometimes stricter about what the `original_range` needs to provide.
The general pattern for creating range adaptors is the same in our library and in the standard library.
However, we aim to establish clearer rules for what you can and cannot do with the `range_adaptor`.
To achieve this, we are sometimes stricter about what the `underlying_range` needs to provide.


### ⌨️ Summary for the casual C++ programmer
Expand All @@ -22,6 +23,7 @@ To achieve this, we are sometimes stricter about what the `original_range` needs
* [Simpler types](./docs/simpler_types.md) and better error messages (at least we are trying :sweat_smile:).
* Less confusion: you don't need to understand what a "view" is, because it is irrelevant for this library.


### 🤓 Summary for the Ranges nerd

This library [fundamentally differentiates between multi-pass and single-pass ranges](./docs/range_properties.md).
Expand All @@ -36,28 +38,31 @@ This library [fundamentally differentiates between multi-pass and single-pass ra
* This results in simpler types, fewer nested template instantiations, and avoids lots of special cases in the multi-pass implementations.


## 📖 Further reading

**Please have a look at the docs folder.**
In particular, you may be interested in:

* [Getting started](./docs/getting_started.md): short introduction on how to use this library and the terminology used in the documentation.
* [Implementation status](./docs/implementation_status.md): overview of which adaptors are already available.
* [Examples](./docs/examples.md): examples that illustrate standard library usage vs radr usage ("tony tables").
* [Trade-offs](./docs/tradeoffs.md): things to be aware before switching to this library.

## 🗒️ Library facts

* Easy to use: header-only and no dependencies
* License: Apache with LLVM exception[^boost]
* Compilers: GCC≥11 or Clang≥17
* Standard libraries: both libstdc++ and libc++ are tested.
* C++20 required.[^std]
* Progress: all std::views from C++20 are reimplemented and many later ones
* (Only) C++20 required.[^std]

[^boost]: The file `generator.hpp` is licensed under the Boost Software license. It is used only if your standard library does not provide `std::generator`.

[^std]: A bonus of using this library is getting access to the equivalent of C++23 and C++26 adaptors in a C++20-based codebase.


## 📖 Further reading

**Please have a look at the docs folder.**
In particular, you may be interested in:

* [Getting started](./docs/getting_started.md): short introduction on how to use this library and the terminology used in the documentation.
* [Implementation status](./docs/implementation_status.md): overview of which adaptors are already available.
* [Examples](./docs/examples.md): examples that illustrate standard library usage vs radr usage ("tony tables").
* [Trade-offs](./docs/tradeoffs.md): things to be aware before switching to this library.


## 👪 Credits

Not everything presented here is novel—in fact, many of the ideas are based on older "ranges" designs (e.g. old ISO papers, Boost ranges and old range-v3).
Expand Down
14 changes: 8 additions & 6 deletions docs/getting_started.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

The usage patterns of this library are almost identical to the standard library.

Differences exist in handling some underlying ranges.
* Just replace `std::views::FOO` with `radr::FOO`; see below for slight differences in range capture.
* You can use all functions, algorithms and concepts from `std::ranges::` on our types.
* Optionally, you can use `radr::begin()` / `radr::end()` instead of `std::ranges::begin()` / `std::ranges::end()`.

## Quickstart
## Capturing the underlying range

```cpp
/* temporaries */
Expand All @@ -18,13 +20,13 @@ auto rad1 = std::move(vec1) | radr::take(2); // this library

/* refer to existing borrowed range */
std::span s = vec1;
auto vue2 = s | std::views::take(2); // standard library
auto rad2 = s | radr::take(2); // this library
auto vue2 = s | std::views::take(2); // standard library
auto rad2 = s | radr::take(2); // this library

/* refer to existing container */
std::vector vec2{1, 2, 3};
auto vue3 = vec2 | std::views::take(2); // standard library
auto rad3 = std::ref(vec2) | radr::take(2); // this library DIFFERENCE
auto rad3 = std::ref(vec2) | radr::take(2); // this library ← ONLY DIFFERENCE!
```

As you can see, only the syntax for creating an indirection is slightly different, i.e. when adapting an existing container, you need to explicitly state whether you want to `std::move()` or `std::ref()` it.
Expand All @@ -49,5 +51,5 @@ the *original range*.
Our range adaptors can be created on any of these three categories, and will result in an adapted range of the same
category, except that `std::ref()`-ing a container leads to a borrowed range (see the last example above).

In particular, it should be noted that *adapted ranges* are not a separate category and no range properties are specific to them; e.g. an adaptor on a `std::string_view` (a borrowed range) is also just a borrowed range—while an adaptor on a `std::vector` (first example above) is also just a "container".
In particular, it should be noted that *range adaptors* are not a separate category and no range properties are specific to them; e.g. an adaptor on a `std::string_view` (a borrowed range) is also just a borrowed range—while an adaptor on a `std::vector` (first example above) is also just a "container".
We do not use the term "view" and the formal and informal definitions of "view" and "viewable range" are irrelevant for this library.
25 changes: 15 additions & 10 deletions docs/implementation_status.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ This library is a **work-in-progress**.
We try to stay close to standard library interfaces, but we neither promise full API compatibility with `std::ranges::` nor do we currently promise stability between releases of this library.
The latter will likely change at some point.


## Range adaptor classes


Expand All @@ -16,6 +17,7 @@ The latter will likely change at some point.
There are no distinct type templates per adaptor (like e.g. `std::ranges::transform_view` for `std::views::transform` in the standard library).
Instead all range adaptor objects in this library (see below) return a specialisation of one of the above type templates.


## Range adaptor objects

We plan to add equivalent objects for all standard library adaptors.
Expand All @@ -33,6 +35,7 @@ We plan to add equivalent objects for all standard library adaptors.
| `radr::reverse` | C++20 | | `std::views::reverse` | C++20 | |
| `radr::slice(m, n)` | C++20 | | *not yet available* | | get subrange between m and n |
| `radr::split(pat)` | C++20 | | `std::views::split` | C++20 | |
| *not planned* | C++20 | | `std::views::lazy_split` | C++20 | use `radr::to_single_pass | radr::split` |
| `radr::take(n)` | C++20 | | `std::views::take` | C++20 | |
| `radr::take_exactly(n)` | C++20 | | *not yet available* | | turns unsized into sized |
| `radr::take_while(fn)` | C++20 | | `std::views::take_while` | C++20 | |
Expand All @@ -41,22 +44,24 @@ We plan to add equivalent objects for all standard library adaptors.
| `radr::transform(fn)` | C++20 | | `std::views::transform` | C++20 | |
| `radr::values` | C++20 | | `std::views::values` | C++20 | |


All range adaptors from this library are available in C++20, although `radr::as_rvalue` behaves slightly different between modes.

[^diff]: These range adaptors have relevant differences between `std::` and `radr::`. Usually the names have been chosen differently to highlight this.


## Range factory objects

| Range factories | Equivalent in `std::` | Remarks |
|-------------------------------|-------------------------|------------------------------------------------------|
| `radr::empty<T>` | `std::views::empty` | |
| `radr::iota(val[, bound])` | `std::views::iota` | multi-pass version of iota |
| `radr::iota_sp(val[, bound])` | `std::views::iota` | single-pass version of iota |
| `radr::istream<Val>` | `std::views::istream` | |
| `radr::istream<Val>` | `std::views::istream` | |
| `radr::repeat(val[, bound])` | `std::views::repeat` | allows indirect storage and static bounds |
| `radr::single(val)` | `std::views::single` | allows indirect storage |
| Range factories (objects) | C++XY | | Equivalent in `std::` | C++XY | Remarks |
|-------------------------------|-------|-|-------------------------|-----------|-------------------------------------------|
| `radr::counted(it, n)` | C++20 | | `std::views::counted` | C++20 | multi-pass version of counted |
| `radr::counted_sp(it, n)` | C++20 | | `std::views::counted` | C++20 | single-pass version of counted |
| `radr::empty<T>` | C++20 | | `std::views::empty` | C++20 | |
| `radr::iota(val[, bound])` | C++20 | | `std::views::iota` | C++20 | multi-pass version of iota |
| `radr::iota_sp(val[, bound])` | C++20 | | `std::views::iota` | C++20 | single-pass version of iota |
| `radr::istream<Val>` | C++20 | | `std::views::istream` | C++20 | |
| `radr::repeat(val[, bound])` | C++20 | | `std::views::repeat` | **C++23** | allows indirect storage and static bounds |
| `radr::single(val)` | C++20 | | `std::views::single` | C++20 | allows indirect storage |


## Notable functions

Expand Down
86 changes: 86 additions & 0 deletions include/radr/factory/counted.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// -*- C++ -*-
//===----------------------------------------------------------------------===//
//
// Copyright (c) 2023-2025 Hannes Hauswedell
//
// Licensed under the Apache License v2.0 with LLVM Exceptions.
// See the LICENSE file for details.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//

#pragma once

#include <iterator>

#include "../rad/take.hpp"

namespace radr
{

inline namespace cpo
{

/*!\brief Create a range from an iterator and a count.
* \tparam It The type of \p it; required to model std::forward_iterator.
* \param[in] it The iterator.
* \param[in] n The count.
* \details
*
* In contrast to std::views::counted, this factory always returns a multi-pass range and thus requires
* `std::forward_iterator<It>` and not just `std::input_or_output_iterator<It>`.
*
* There is radr::counted_sp which is always a single-pass range and does not have this requirement.
*
* Unlike std::views::counted, we consider radr::counted a range factory and not a range adaptor, because
* you cannot pipe a range into it.
*
* ### Safety
*
* Note that this function (like std::views::counted) performs no bounds-checking.
*/
inline constexpr auto counted = []<std::forward_iterator It>(It it, size_t const n)
{
/* this is a bit of a hack; the sentinels are irrelevant and also the first size parameter
* but take_borrow will dispatch to either subborrow (for ra-iter) or else
* (std::counted_iterator, default_sentinel) which is what we want.
*/
return detail::take_borrow(
borrowing_rad{it, std::unreachable_sentinel, make_const_iterator(it), std::unreachable_sentinel, n},
n);
};

/*!\brief Single-pass version of radr::counted.
* \tparam It The type of \p it; required to model std::input_or_output_iterator.
* \param[in,out] it The iterator.
* \param[in] n The count.
* \details
*
* This factory always returns a radr::generator, a move-only, single-pass range.
*
* The requirements on the iterator type are weaker than for radr::counted.
*
* ### Safety
*
* Note that this function (like std::views::counted) performs no bounds-checking.
*/
inline constexpr auto counted_sp = []<typename It>(It && it, size_t const n)
{
static_assert(std::input_or_output_iterator<std::remove_cvref_t<It>>,
"You must pass an iterator as first argument to radr::counted_sp().");

// Intentionally, no forwarding reference in this signature
return [](It it, size_t const n) -> generator<std::iter_reference_t<It>, std::iter_value_t<It>>
{
uint64_t i = 0;
while (i < n)
{
co_yield *it;
++i;
++it;
}
}(std::forward<It>(it), n);
};

} // namespace cpo
} // namespace radr
50 changes: 19 additions & 31 deletions include/radr/factory/iota.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,12 @@ using wider_signed_t = std::conditional_t<sizeof(Int) < sizeof(short), short,
long long>>>;
// clang-format on

// iota_diff_t
template <class Start>
using iota_diff_t =
std::conditional_t<(!std::is_integral_v<Start> || sizeof(std::iter_difference_t<Start>) > sizeof(Start)),
std::iter_difference_t<Start>,
wider_signed_t<Start>>;

// __decrementable, __advanceable
template <class Iter>
concept decrementable = std::incrementable<Iter> && requires(Iter i) {
{
Expand Down Expand Up @@ -66,18 +64,18 @@ concept advanceable =
} -> std::convertible_to<iota_diff_t<Iter>>;
};

// Iterator
template <std::incrementable Start>
class iota_iterator
{
private:
Start value_{};

public:
using value_type = Start;
using difference_type = iota_diff_t<value_type>;

using value_type = Start;
using difference_type = iota_diff_t<value_type>;
using iterator_category = std::input_iterator_tag;
// clang-format off
using iterator_concept =
using iterator_concept =
std::conditional_t<advanceable<value_type>, std::random_access_iterator_tag,
std::conditional_t<decrementable<value_type>, std::bidirectional_iterator_tag,
std::forward_iterator_tag>>;
Expand Down Expand Up @@ -228,26 +226,27 @@ class iota_iterator
}
};

template <typename Start, typename BoundSentinel>
template <typename Start, typename Bound>
class iota_sentinel
{
BoundSentinel bound_{};
private:
Bound bound_{};

public:
constexpr iota_sentinel() = default;
constexpr explicit iota_sentinel(BoundSentinel b) : bound_(std::move(b)) {}
constexpr explicit iota_sentinel(Bound b) : bound_(std::move(b)) {}

friend constexpr bool operator==(iota_iterator<Start> const & it, iota_sentinel const & sent)
{
return *it == sent.bound_;
}
friend constexpr auto operator-(iota_iterator<Start> const & it, iota_sentinel const & sent)
requires std::sized_sentinel_for<BoundSentinel, Start>
requires std::sized_sentinel_for<Bound, Start>
{
return *it - sent.bound_;
}
friend constexpr auto operator-(iota_sentinel const & sent, iota_iterator<Start> const & it)
requires std::sized_sentinel_for<BoundSentinel, Start>
requires std::sized_sentinel_for<Bound, Start>
{
return -(it - sent);
}
Expand All @@ -266,15 +265,12 @@ struct iota_fn
using It = detail::iota_iterator<Value>;
using Sen = std::conditional_t<std::same_as<Value, Bound>, It, detail::iota_sentinel<Value, Bound>>;

if constexpr ((std::random_access_iterator<It> && std::same_as<It, Sen>) ||
std::sized_sentinel_for<Bound, Value>)
{
return borrowing_rad<It, Sen, It, Sen, borrowing_rad_kind::sized>{It{val}, It{bound}};
}
else
{
return borrowing_rad<It, Sen, It, Sen, borrowing_rad_kind::unsized>{It{val}, Sen{bound}};
}
constexpr borrowing_rad_kind kind =
((std::random_access_iterator<It> && std::same_as<It, Sen>) || std::sized_sentinel_for<Bound, Value>)
? borrowing_rad_kind::sized
: borrowing_rad_kind::unsized;

return borrowing_rad<It, Sen, It, Sen, kind>{It{val}, Sen{bound}};
}
};

Expand Down Expand Up @@ -310,7 +306,7 @@ inline constexpr detail::iota_fn iota{};
* The requirements on the types are weaker than for radr::iota.
*/
inline constexpr auto iota_sp =
detail::overloaded{[]<typename Value, typename Bound>(Value val, Bound bound) -> radr::generator<Value>
[]<typename Value, typename Bound = std::unreachable_sentinel_t>(Value val, Bound bound = {})->radr::generator<Value>
{
static_assert(std::weakly_incrementable<Value>,
"The Value type to radr::iota_sp needs to satisfy std::weakly_incrementable.");
Expand All @@ -322,14 +318,6 @@ inline constexpr auto iota_sp =
co_yield val;
++val;
}
},
[]<typename Value>(Value val) -> radr::generator<Value>
{
while (true)
{
co_yield val;
++val;
}
}};
};

} // namespace radr
2 changes: 1 addition & 1 deletion include/radr/rad_util/borrowing_rad.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ class borrowing_rad : public rad_interface<borrowing_rad<Iter, Sent, CIter, CSen
}

public:
borrowing_rad() = default;
constexpr borrowing_rad() = default;

/*!\name Constructors: Iterator, Sentinel
* \{
Expand Down
1 change: 1 addition & 0 deletions tests/unit/factory/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
radr_unit_test(counted)
radr_unit_test(empty)
radr_unit_test(iota)
radr_unit_test(istream)
Expand Down
Loading