Search code examples
c++std-rangesc++23

What is the meaning of default_constructible range adaptors in C++23?


Since C++23, views 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 ranges 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 views 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 views 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 views:

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?


Solution

  • 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 transforming 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.