Search code examples
c++c++20std-ranges

Why can't I pass a std::views::join by const reference?


I have the code below (also on compiler explorer) and I'm confused about why the const Range & versions don't work. The compiler complains that std::range::begin (and end) don't work.

This is actually an XY problem so I'll also mention how I got here: I'm writing some unit tests on range adapters and I wrote a matcher in googletest for comparing two ranges: MATCHER_P(RangeEq, value, "") { return std::ranges::equal(arg, value); }

This worked fine until I got to an adapter that returned a join. As I tried to figure this out I realized that I can't seem to take the range as a const& and maybe that's the same problem inside the googletest matcher.

#include <algorithm>
#include <iostream>
#include <ranges>
#include <string>
#include <vector>

template <std::ranges::range Range>
auto print1(std::ostream& out, Range&& range) {
    out << '[';
    auto begin = std::ranges::begin(std::forward<Range>(range));
    auto end = std::ranges::end(std::forward<Range>(range));

    if (begin != end) {
        out << *begin;
        ++begin;
    }
    while (begin != end) {
        out << ", " << *begin;
        ++begin;
    }
    out << ']' << std::endl;
}

template <std::ranges::range Range>
auto print2(std::ostream& out, const Range& range) {
    out << '[';
    auto begin = std::ranges::begin(range);
    auto end = std::ranges::end(range);

    if (begin != end) {
        out << *begin;
        ++begin;
    }
    while (begin != end) {
        out << ", " << *begin;
        ++begin;
    }
    out << ']' << std::endl;
}

template <std::ranges::range Lhs, std::ranges::range Rhs>
bool equal1(Lhs&& lhs, Rhs&& rhs) {
    return std::ranges::equal(std::forward<Lhs>(lhs), std::forward<Rhs>(rhs));
}

template <std::ranges::range Lhs, std::ranges::range Rhs>
bool equal2(const Lhs& lhs, const Rhs& rhs) {
    return std::ranges::equal(lhs, rhs);
}

int main() {
    std::cout << std::boolalpha;

    auto view = std::views::iota(0, 5) | std::views::transform([](auto num) {
                    return std::views::iota(0, num);
                }) |
                std::views::join;

    print1(std::cout, view);
    // print2(std::cout, view); // compile error, why?

    auto nums = std::vector<int>{0, 0, 1, 0, 1, 2, 0, 1, 2, 3};

    std::cout << equal1(view, nums) << std::endl;
    // std::cout << equal2(view, nums) << std::endl; // compile error, why?

    return 0;
}


Solution

  • When the reference of the nested range you join is a prvalue range, join_view needs to temporarily store the entire prvalue range into its internal member, which makes its begin() not const-qualified since the iterator will modify this cached member during construction.

    That is, in your example join_view only has non-const-qualified begin(), which makes its const objects not satisfy the range concept.

    A representative non-const iterable range adaptor is filter_view, which always does not have the const begin due to time complexity requirements. However, join_view conditionally provides it as most range adaptors do (although unfortunately not in your case).