Since C++23, view
s are no longer required to be default_constructible
. For range adaptors such as views::filter
and views::transform
, their default constructor is redefined as:
template<input_range V, indirect_unary_predicate<iterator_t<V>> Pred>
requires view<V> && is_object_v<Pred>
class filter_view : public view_interface<filter_view<V, Pred>> {
private:
V base_ = V(); // exposition only
copyable-box<Pred> pred_; // exposition only
public:
filter_view() requires default_initializable<V> && default_initializable<Pred> = default;
};
And because the default constructor of ref_view
has been deleted in p2325r3, this indicates that range adapters applied into lvalue range
s such as std::vector
are no longer default_constructible
:
std::vector v{1, 2, 3};
auto r = v | std::views::filter([](auto) { return true; });
decltype(r){}; // ok in C++20, error in C++23
Only when range adapters are applied into default_constructible
view
s such as std::string_view
or views::iota(0)
, the returned views can be default_constructible
:
auto r = std::views::iota(0) | std::views::filter([](auto) { return true; });
decltype(r){}; // ok in C++20 and C++23
But regarding these cases, I really can’t think of the use cases to make these view
s default_constructible
even if that is feasible. If we construct a view
by default, it means that we construct an empty view
, and it is obviously meaningless to apply range adaptors to the empty view
s:
constexpr auto r = std::views::iota(0, 10)
| std::views::transform([](auto x) { return x; });
static_assert(!std::ranges::empty(r));
// views::iota is default-constructible, so r is also default-constructible
static_assert( std::ranges::empty(decltype(r){}));
In my opinion, the default constructor of range adaptors in C++20 is just to satisfy the view
, since the view
is no longer required to be default_constructible
in C++23, there is no need for the default constructors of these adapters to existing.
Why is the default constructor of these range adaptors not deleted in C++23 but make into a constraint function? Is there really a case that requires it to be default_constructible
? What are the considerations behind this?
The status quo before this paper is that views simply have to be default constructible, even if that isn't a meaningful requirement that can be fulfilled by the view, leading it to have a singular state that can only be assigned to. That wasn't very useful (and indeed harmful), which is why that requirement was removed.
But now that views simply can be default constructible, there is the question of when you should make a range adaptor default constructible. And the answer to that question is more straightforward: why wouldn't you make it default constructible if you could?
If you're transform
ing a view, V
, that is default constructible (and presumably V
is default constructible in a way that has a meaningful state - which isn't infeasible: a default-constructed string_view
is a valid empty range) and your function F
is also default constructible (as, say, any capture-less lambda is in C++20), then transform_view<V, F>
really should also be default constructible. There really isn't much reason for it not to be, it's effectively free to conditionally provide that constructor, and it can't cause harm.
So between not providing a default constructor and conditionally providing a default constructor where it would be sensible to do so, the latter just seems like a more user-friendly choice.
Otherwise, from the perspective of the library, you'd have to be able to somehow guarantee that default constructing such range adaptors would never be useful... and I'm not sure you could do that?
Note that this is not a new library feature for C++23, but rather a defect against C++20.