P2415R2: What is a view?

P2415R2 is a proposal for C++20 that relaxes the move concept to support types that are not constant-time destructible while still guarantee the cheap construction of range adaptors. It introduces a owning_view class template, enabling views::all to support movable rvalue non-view ranges.

  • P0896R4 introduces the view concept. It states that a type T models view if it's default constructible, constant-time copyable, and constant-time movable.
  • P1456R1 relaxes the view concept. It states that a type T models view if it's default constructible, constant-time movable, and constant-time destructible. If T is copyable, it should be constant-time copyable.
  • P2325R3 relaxes the view concept. It states that a type T models view if it's constant-time movable and constant-time destructible. If T is copyable, it should be constant-time copyable.

Motivation

The initial motivation of the constant-time copyable constraint is that the range-based algorithms might take views by value. However, if they are implemented as this, they won't be able to operate directly on ranges. (For example, ranges::reverse(v) would be invalid if v is a std::vector<T>.) Therefore, they are revised to take ranges by forwarding references.

template <ranges::bidirectional_range R>
  requires std::permutable<ranges::iterator_t<R>>
constexpr ranges::borrowed_iterator_t<R> reverse(R &&r);

The author states that the actual motivation for the constant-time copyable constraint is that range adaptors, such as transform_view, still take views by value. If the constraint is relaxed, constructing a chain of range adaptors might be an expensive operation. Because the range adaptors are lazily-evaluated, their constructions should be cheap. Otherwise, users bear the runtime cost without actual work being done at that point. The author concludes that the goal of the view concept is to guarantee the cheap construction of range adaptors.

Proposal

The author proposes a scenario that satisfies the cheap construction of range adaptors while violating the existing constraints of the view concept. For example, the bad_view class is non-copyable, constant-time movable, and linear-time destructible, which doesn't model view. However, constructing a range adaptor from a bad_view is no more expensive than constructing one from a ref_view<vector<uint64_t>>. Moreover, a linear-time destruction of vector<uint64_t> is required in either case, so bad_view's linear-time destructor doesn't affect the performance.

class bad_view : public std::ranges::view_interface<bad_view> {
public:
  bad_view(std::vector<std::uint64_t> v) : data_(std::move(v)) {}

  bad_view(bad_view const &) = delete;
  bad_view(bad_view &&) = default;
  bad_view &operator=(bad_view const &) = delete;
  bad_view &operator=(bad_view &&) = default;

  auto begin() { return data_.begin(); }
  auto end() { return data_.end(); }

private:
  std::vector<std::uint64_t> data_;
};

static_assert(std::ranges::view<bad_view>);

The author concludes that the requirement of constant-time destruction can be relaxed without violating the goal of the view concept. The requirement is reformulated such that if N moves are made from an object of type T that containes M elements, then these N objects have O(N + M) destruction. It implies that the moved-from object is constant-time destructible.

Based on the revisited requirement, the author proposes to introduce the owning_view, which is a view that has unique ownership of a range. It is movable and stores the range within it. It will still guarantee the cheap construction of range adaptors. Moreover, the author proposes to revise the view::all range adaptor so that it will capture the ownership of movable rvalue non-view ranges through owning_view. The viewable_range concept is updated to accept this type of range.

In the end, the author attempts to define what is a view. This is exemplified through the code snippet: auto rng = v | views::reverse;. If v is an lvalue, the decision hinges on whether rng should copy v or refer to it. If copying v is preferred because it's a cheap operation and it can avoid potential dangling, then v is considered a view. Conversely, if referencing v is more practical, particularly when copying v is expensive, then v is not a view. For instance, string_view is a view, while string is not.