P0896R4: The One Ranges Proposal
P0896R4 is a proposal for C++20 that introduces a new header <ranges>
and a set of utilities and concepts related to range-based programming. The main goal of this proposal is to provide a unified and consistent approach to working with ranges, which are sequences of elements that can be iterated over.
#include <ranges>
#include <vector>
const std::vector<std::uint64_t> even_square =
std::views::iota(static_cast<std::uint64_t>(1)) | std::views::take(10) |
std::views::filter([](const std::uint64_t element) static noexcept -> bool {
return element % 2 == 0;
}) |
std::views::transform(
[](const std::uint64_t element) static noexcept -> std::uint64_t {
return element * element;
}
) |
std::ranges::to<std::vector<std::uint64_t>>();
Ranges Concepts
range
template <typename T>
concept range = requires(T &t) {
ranges::begin(t);
ranges::end(t);
};
The range
concept specifies the requirements for a type T
to be considered a range. T
must provide an iterator and a sentinel, representing the begin and end of the range. Both ranges::begin(T)
and ranges::end(T)
must be constant and amortized constant time operations.
sized_range
template <typename T>
concept sized_range = ranges::range<T> && requires(T &t) { ranges::size(t); };
template <typename> inline constexpr bool disable_sized_range = false;
The sized_range
concept specifies the requirements of a range
type that knows its size in constant time with the ranges::size
function.
However, there are types that provide a size
function but don't model sized_range
. For instance, a pre-C++11 std::list
has a linear-time size
method. The disable_sized_range
variable template exists to allow such types to opt out of being considered as sized_range
.
borrowed_range
template <typename R>
concept borrowed_range =
ranges::range<R> &&
(std::is_lvalue_reference_v<R> || ranges::enable_borrowed_range<std::remove_cvref_t<R>>);
The borrowed_range
concept specifies the requirements of a range
type that it doesn't own its data, so a function can take it and return iterators obtained from it without danger of dangling. All rvalue ranges are assumed to be non-borrowing, except for these non-owning types, such as std::string_view
, std::span
, and ranges::subrange
.
When an algorithm that returns an iterator or a subrange
of a range that takes a particular rvalue range argument that doesn't model borrowed_range
, the ranges::dangling
placeholder type will be returned to avoid returning dangling iterators. ranges::dangling
can't be dereferenced.
// `it` is an iterator
std::vector<int> v;
auto it = ranges::find(v, 99);
// `it` is `ranges::dangling`
auto it = ranges::find(std::vector<int>(), 99);
// `it` is an iterator
std::vector<int> v;
auto it = ranges::find(std::span(v), 99);
view
template <typename T>
concept view = ranges::range<T> && std::movable<T> && ranges::enable_view<T>;
The view
concept specifies the requirements of a range
type that has suitable semantic properties for use in constructing range adaptor pipelines. T
models view
if its move operations and destruction are constant, such as std::string_view
, std::span
, and ranges::subrange
.
The enable_view<T>
variable template is used to indicate whether a range is a view. It evaluates to true
if either T
is derived from view_base
or T
has a public base class view_interface<U>
and doesn't have base classes of type view_interface<V>
.
viewable_range
template <typename T>
concept viewable_range =
ranges::range<T> && ((ranges::view<std::remove_cvref_t<T>> &&
std::constructible_from<std::remove_cvref_t<T>, T>) ||
(!ranges::view<std::remove_cvref_t<T>> &&
(std::is_lvalue_reference_v<T> ||
(std::movable<std::remove_reference_t<T>> &&
!/*is-initializer-list*/<T>))));
The viewable_range
concept specifies the requirements of a range
type that can be converted into a view
through ranges::view_all
.
common_range
template <typename T>
concept common_range =
ranges::range<T> &&
std::same_as<ranges::iterator_t<T>, ranges::sentinel_t<T>>;
The common_range
concept specifies the requirements of a range
type for which ranges::begin()
and ranges::end()
return the same type.
Ranges Access
The proposal introduces several utilities that retrieve certain properties from a range.
ranges::begin
returns an iterator to the beginning of a range.ranges::end
returns a sentinel indicating the end of a range.ranges::rbegin
returns a reverse iterator to a range.ranges::rend
returns a reverse end iterator to a range.ranges::size
returns an integer equal to the size of a range.ranges::data
obtains a pointer to the beginning of a contiguous range.
These functions are implemented as customization point objects, which will attempt to find user-provided customized implementations before falling back to a default implementation. For example, for an element t
with type T
, ranges::size(t)
will attempt to invoke t.size()
and size(t)
. If these invocations are not available, it will attempt to calculate ranges::end(t) - ranges::begin(t)
and cast the result to an unsigned integer-like type. Therefore, to customize the behavior of ranges::size
, T
can provide a size
function that is either a member or a non-member. The following code snippet is a simplified implementation of ranges::size
:
template <typename T>
concept size_enabled =
!ranges::disable_sized_range<std::remove_cvref_t<T>>;
template <typename T>
concept member_size = size_enabled<T> && requires(T &&t) {
{ auto(t.size()) } -> std::integral;
};
template <typename T>
concept unqualified_size = size_enabled<T> && requires(T &&t) {
{ auto(size(t)) } -> std::integral;
};
template <typename T>
concept iter_difference =
!member_size<T> && !unqualified_size<T> && requires(T &&t) {
{ ranges::begin(t) } -> std::forward_iterator;
{
ranges::end(t)
} -> std::sized_sentinel_for<decltype(ranges::begin(std::declval<T>()
))>;
};
struct size_fn {
// `auto(t.size())` is a valid expression
template <typename T>
requires member_size<T>
[[nodiscard]] constexpr auto static operator()(T &&t
) noexcept(noexcept(auto(t.size()))) {
return auto(t.size());
}
// `auto(size(t))` is a valid expression
template <typename T>
requires unqualified_size<T>
[[nodiscard]] constexpr auto static operator()(T &&t
) noexcept(noexcept(auto(size(t)))) {
return auto(size(t));
}
// default implementation
template <typename T>
requires iter_difference<T>
[[nodiscard]] constexpr auto static operator()(T &&t
) noexcept(noexcept(auto(size(t)))) {
return static_cast<std::uint64_t>(
ranges::end(t) - ranges::begin(t)
);
}
/* ... */
};
inline constexpr auto size = size_fn{};
Ranges Utilities
view_interface
template <typename D>
requires std::is_class_v<D> && std::same_as<D, std::remove_cv_t<D>>
class view_interface;
The view_interface
CRTP-based class template defines a view interface. The derived class is required to define a begin
member function and an end
member function, and view_interface
will provide several other functions using these member functions. The following code snippet is a simplified implementation of view_interface
:
template <typename D>
requires std::is_class_v<D> && std::same_as<D, std::remove_cv_t<D>>
class view_interface {
private:
constexpr D &derived() noexcept { return static_cast<D &>(*this); }
public:
constexpr bool empty()
requires std::ranges::sized_range<D> || std::ranges::forward_range<D>
{
if constexpr (std::ranges::sized_range<D>) {
return std::ranges::size(derived()) == 0;
} else {
return std::ranges::begin(derived()) == std::ranges::end(derived());
}
}
/* ... */
};
subrange
enum class subrange_kind : bool { unsized, sized };
template <
std::input_or_output_iterator I, std::sentinel_for<I> S = I,
ranges::subrange_kind K = std::sized_sentinel_for<S, I>
? ranges::subrange_kind::sized
: ranges::subrange_kind::unsized>
requires(K == ranges::subrange_kind::sized || !std::sized_sentinel_for<S, I>)
class subrange : public ranges::view_interface<subrange<I, S, K>>;
The subrange
class template combines together an iterator and a sentinel into a single view
. The subrange
is a sized_range
whenever the final template parameter is subrange_kind::sized
. The size record is needed to be stored if std::sized_sentinel_for<S, I>
is false
and K
is subrange_kind::sized
. The constraint prohibits the scenario when K
is subrange_kind::unsized
and std::sized_sentinel_for<S, I>
is true
.
auto main() -> std::int32_t {
std::vector<std::uint8_t> buf(512);
std::ranges::subrange buf_view(buf);
static_assert(sizeof(buf_view) == 16);
return 0;
}
Ranges Adaptors
ranges::ref_view
template <ranges::range R>
requires std::is_object_v<R>
class ref_view : public ranges::view_interface<ref_view<R>>;
The ref_view
class template is a view
of the elements of some range. It wraps a reference to that range.
ranges::owning_view
template <ranges::range R>
requires std::movable<R> && (!/*is-initializer-list*/<R>)
class owning_view : public ranges::view_interface<owning_view<R>>;
The owning_view
class template is a view
that has unique ownership of a range. It is movable and stores the range within it. Its constructor accepts an rvalue reference and will move it into the stored range.
views::all
The views::all
range adaptor closure object returns a view
that includes all elements of its range argument.
- If
std::decay_t<T>
modelsview
,ranges::view_all
will convert the range into astd::decay_t<T>
prvalue. - If
T
is an lvalue reference,ranges::view_all
will return aref_view
that refers to the range. - If
T
is a movable,ranges::view_all
will return anowning_view
that takes ownership of the range.