I am unable to construct a vector using a iterators over a view that uses take_while in its construction

115 views Asked by At

I ran into this problem while trying to make a powerset function using the std::views library.


auto set = sv::repeat(0) 
    | sv::take_while([&mask](...) { return mask > 0; })
    | sv::transform([s, &mask, idx = 0](...) mutable { 
        return idx = std::countr_zero(mask & ~(mask - 1)), mask &= mask - 1, s[idx]; });
auto vec = std::vector(set.begin(), set.end());
           ^__________ ^______________________
"*cannot deduce types*"   "*no constructor found*" <- these are rough translations of verbose diagnostics

My question is, why doesn't this work? Normally I can construct vectors using views like this perfectly well, and I even do it later in the function. I tested the same thing, but this time I used a captureless and const lambda:

auto arr = std::array {1, 2, 3, 3, 4, 5, 6, 1, 2, 3};
auto arr_view = arr | std::views::take_while([](int i) { return i < 4; });
auto vec = std::vector(arr_view.begin(), arr_view.end());

and got identical error messages. Stranger still, I tried using the std::ranges::move() function to do the same thing, and it worked for some reason! This code compiles and runs as expected (both with the constructed view as an rvalue and stored in a variable):

auto arr_view = arr | std::views::take_while([](int i) { return i < 4; });
auto vec = std::vector<int>{};
std::ranges::move(arr_view, std::back_inserter(vec));

Initially, I thought this was because "arr_view" wasn't a sized range, but the same iterative approach of construction works for std::views::filter(), which is also not a sized range. Can someone explain to me the reasoning behind this not working? Is it just me using (unstable) modern features?

1

There are 1 answers

0
0xbachmann On

set.begin() and set.end() return iterators of different types, i.e. begin() returns an iterator while end() returns a sentinel. This is necessary for take_while, because one does not know beforehand how long to take. the sentinel returned by end() will compare equal to the iterator, once the predicate of take_while evaluates to false.

To construct a std::vector from a range with a sentinel there are different methods:

  1. views::common makes a view into a common view i.e. with different type iterators:
auto set1 = sv::iota(0)
    | sv::take_while([](auto counter) { return counter < 5; })
    | sv::common;
auto vec1 = std::vector(set.begin(), set1.end());
  1. std::vectors new constructor taking a range. unfortunately, it needs a second argument to allow for the correct constructor to be chosen thus an object of std::from_range_t needs to be passed as the first argument.
auto set2 = sv::iota(0)
    | sv::take_while([](auto counter) { return counter < 5; });
auto vec2 = std::vector(std::from_range, set2);
  1. using std::ranges::to to convert a range to a container:
auto vec3 = sv::iota(0)
    | sv::take_while([](auto counter) { return counter < 5; })
    | std::ranges::to<std::vector>();

See Demo

Note: depending on your different methods might be implemented (e.g. in the demo clang trunk needs -stdlib=libc++ to compile with its own standard library)