What is the use of forwarding references in deduction guides?

2 weeks ago 10
ARTICLE AD BOX

First of all, the line

constexpr ContainerView(Range r) : range_(std::move(r)), begin_(std::begin(r)), end_(std::end(r)) {}

is completely wrong (surely it won't work with owning_view). Members of class are initialized in their order in class defintion, so initialization of range_ happens first, so after r was moved, what std::begin(r) and std::end(r) do ? The valid version is:

constexpr ContainerView(Range r) : range_(std::move(r)), begin_(std::begin(range_)), end_(std::end(range_)) {}

The way forwarding reference works is simple:

template< class T > void bar (T&& t) {}

if passed value is lvalue, T is deduced to be T&, otherwise it is still T.


std::views::all_t gives you:

a view, if passed range is already view (it is not your case) ref_view is passed viewable range, is lvalue. ref_view just works as reference to passed container. owing_view, if passed viewable range is prvalue or xvalue. Passed viewable range is moved into resulting view. As the name suggests, the view becomes an owner of passed viewable range.

Why was the deducation guide taking forwarding reference ContainerView used ? Because it can handle all cases:

accepts lvalue (then ref_view as range is deduced)

accepts prvalue and xvalue (then owing_view as range is deduced)

int main() { std::vector<int> vec; ContainerView view1{ vec }; // lvalue ContainerView view2{ std::vector<int>{} }; // prvalue ContainerView view3{ std::move(vec) }; // xvalue

Version taking just Range works only for prvalue and xvalue. Why doesn't it work for lvalue ? In this case, range is deduced to be owning_view. But constructor of owning_view takes passed viewable object by rvalue reference, so we would need to write:

ContainerView view2{ std::vector<int>{} }; // prvalue ContainerView view3{ std::move(vec) }; // xvalue

but below doesn't compile because vec is lvalue, and it is not accepted by owning_view(R&&).

ContainerView view1{ vec }; // lvalue, error

Without the overload taking forwarding refernce, you would have two overloads to cover all use cases:

// creates ref_view, accepts lvalue template<typename Range> ContainerView(Range& range) -> ContainerView<std::ranges::views::all_t<Range&>>; // creates owning_view, accepts prvalue and xvalue template<typename Range> ContainerView(Range&& range) -> ContainerView<std::ranges::views::all_t<Range>>;

Details required to fully understand how all_t and ref_take work:

template< ranges::viewable_range R > using all_t = decltype(views::all(std::declval<R>())); // [1]

taken from. declval adds rvalue reference to R. If R is lvalue reference, based on reference collapsing we finally get R&, otherwise we have R&&.

And ref_view has deduction guide which accepts only lvalue reference:

template< class R > ref_view( R& ) -> ref_view<R>;

also there is ref_view constructor accepting only lvalue reference.

In the case

template<typename Range> ContainerView(Range range) -> ContainerView<std::ranges::views::all_t<Range>>;

the line [1] takes Range&& that is not acceptable by ref_view so as a result we got owning_view as range which accepts only xvalue/prvalue.

Read Entire Article