P1456R1: Move-only views
P1456R1 is a proposal for C++20 that relaxes the move
concept to support move-only types, which will enable a few useful range
types to model view
, including std::generator
.
Introduction
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. These requirements allow view
to be copied through the composition of range adaptors.
The copyable requirement implies that all data members of a view
should be copyable. However, some range adaptors only require these data members to be copy-constructible on their interface, such as the element of single_view
and the function object of transform_view
. To turn a copy-constructible type into a copyable type, the implementation wraps the type in a copyable_box
.
copyable_box
The copyable_box
converts a type that is copy constructible (but might not copy assignable) into a type that is both copy constructible and copy assignable, thus copyable.
- If the type is not copyable, the
copyable_box
is trivial. - If the type is not copyable and the copy-constructor is
noexcept
, thecopyable_box
provides a copy-assignment operator that destroys its wrapped data usingstd::destroy_at
and copy-constructs it from the othercopyable_box
usingstd::construct_at
. - If the type is not copyable and the copy-constructor is not
noexcept
, thecopyable_box
holds the wrapped data in astd::optional
and provides a copy-assignment operator that copy-constructs the wrapped data from the othercopyable_box
. If the copy-construction throws, the wrapped data is in an empty state.
The following code snippet is an implementation of copyable_box
where the type is not copyable and the copy-constructor is not noexcept
:
class copy_constructible_t {
public:
copy_constructible_t() = default;
copy_constructible_t(const copy_constructible_t &) = default;
copy_constructible_t &operator=(const copy_constructible_t &) = delete;
};
static_assert(std::copy_constructible<copy_constructible_t>);
static_assert(!std::copyable<copy_constructible_t>);
template <typename T>
requires std::copy_constructible<T>
class copyable_box {
public:
constexpr copyable_box() noexcept(std::is_nothrow_default_constructible_v<T>)
requires std::default_initializable<T>
: val_(std::in_place) {}
template <class... Args>
requires std::is_constructible_v<T, Args...>
constexpr explicit copyable_box(
std::in_place_t, Args &&...args
) noexcept(std::is_nothrow_constructible_v<T, Args...>)
: val_(std::in_place, std::forward<Args>(args)...) {}
copyable_box(const copyable_box &) = default;
constexpr copyable_box &operator=(copyable_box const &other
) noexcept(std::is_nothrow_copy_constructible_v<T>) {
if (this != std::addressof(other)) {
if (other.val_.has_value()) {
val_.emplace(*other.val_);
} else {
val_.reset();
}
}
return *this;
}
private:
std::optional<T> val_;
};
static_assert(std::copyable<copyable_box<copy_constructible_t>>);
Motivation
Range Adaptor
Since C++11, the standard library has been adapted to support move-only types. For example, they can be stored in containers. A notable exception is the standard library algorithms, such as std::find_if
, as they don't support move-only function objects. A simple workaround is to define such a function object as a variable and wrap it in a std::reference_wrapper
.
std::vector<std::uint8_t> buf;
auto move_only_predicate = [capture = std::make_unique<std::uint64_t>(0)](
const std::uint8_t
) noexcept -> bool { return true; };
std::find_if(buf.cbegin(), buf.cend(), std::cref(move_only_predicate));
However, for range adaptors that take a function object such as transform_view
, this workaround is a burden. Because these range adaptors support composition and are lazily-evaluated, ensuring that the composed adaptors don't outlive the function object is a non-trivial task.
Moreover, the range adaptors only copy views when the user calls the base()
member function to retrieve a copy of the underlying view being adapted. None of the adaptors copy views in the process of their normal operation.
Generator
Generators are the intersection of coroutines and ranges. A generator is a coroutine that models the range
concept. Coroutine frames are not copyable resources, so it's not possible to implement a generator that can produce independent copies. Therefore, generators can't model the view
concept unless the copyable constraint is dropped.
std::generator<std::uint64_t> fib() {
std::uint64_t a = 0;
std::uint64_t b = 1;
while (true) {
co_yield std::exchange(a, std::exchange(b, a + b));
}
}
Proposal
The author suggests that the view
concept should be relaxed to support move-only types without excluding any types that model the prior formulation
of the concept. Copyable types that intend to model view
must still be constant-time copyable.
template <typename T>
concept View = std::ranges::range<T> && std::movable<T> &&
std::default_initializable<T> && std::ranges::enable_view<T>;
To redress the problem that the base()
members of the range adaptors return copies of the underlying view, each base()
member is replaced with a const&
-qualified overload that copies the view (if it's copy-constructible) and a &&
-qualified overload that extracts the view from the adaptor.