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

How do I apply modifications to a locally scoped range and return it?


The code below, when compiled under g++-11.3 using --std=c++20 -D_GLIBCXX_DEBUG and executed, produces a bizarre runtime error about iterators. I'm not exactly sure what it means but I suspect it has something to do with the vector range going out of scope when test() returns: range doesn't get moved or copied, rather whatever it is the pipe operator returns just retains a reference to range.

#include <ranges>
#include <unordered_set>
#include <vector>

auto to_unordered_set(auto && range) {
    using r_type = std::ranges::range_value_t<decltype(range)>;
    auto common = range | std::views::common;
    return std::unordered_set<r_type>(std::ranges::begin(common), std::ranges::end(common));
}

auto test() {
    std::vector<int> range {1,2,3,4,5};
    return range | std::ranges::views::transform([](auto x) { return x%2; });
}

int main() {
    auto y = to_unordered_set(test());
    return 0;
}

/*
/opt/compiler-explorer/gcc-11.3.0/include/c++/11.3.0/debug/safe_iterator.h:195:
In function:
    __gnu_debug::_Safe_iterator<_Iterator, _Sequence, 
    _Category>::_Safe_iterator(__gnu_debug::_Safe_iterator<_Iterator, 
    _Sequence, _Category>&&) [with _Iterator = 
    __gnu_cxx::__normal_iterator<int*, std::__cxx1998::vector<int, 
    std::allocator<int> > >; _Sequence = std::__debug::vector<int>; 
    _Category = std::forward_iterator_tag]

Error: attempt to copy-construct an iterator from a singular iterator.

Objects involved in the operation:
    iterator "this" @ 0x0x7ffea2b7a8c0 {
      type = __gnu_cxx::__normal_iterator<int*, std::__cxx1998::vector<int, std::allocator<int> > > (mutable iterator);
      state = singular;
    }
    iterator "other" @ 0x0x7ffea2b7a820 {
      type = __gnu_cxx::__normal_iterator<int*, std::__cxx1998::vector<int, std::allocator<int> > > (mutable iterator);
      state = singular;
      references sequence with type 'std::__debug::vector<int, std::allocator<int> >' @ 0x0x7ffea2b7a8b0
    }
*/

Is there anyway to get something like this to work? I basically want to transform / filter / join / etc.. a range and return it (a copy/move of the entire thing gets returned, both the range and whatever modifications get applied to it).


Solution

  • First, since the std::vector itself is common_range, transform_view will also be common_range, so using views::common here is redundant.

    Second and more important, range is a local variable, so it will be destroyed once it leaves test(), which makes test() return a transform_view that holds a dangling pointer.

    Is there anyway to get something like this to work?

    Thanks to P2415, you can directly return a transform_view applied to a rvalue vector, which will construct an owning_view that transfers ownership of the original vector's contents to itself, which will no longer cause dangling.

    #include <ranges>
    #include <unordered_set>
    #include <vector>
    
    auto to_unordered_set(auto && range) {
      using r_type = std::ranges::range_value_t<decltype(range)>;
      return std::unordered_set<r_type>(
               std::ranges::begin(range), std::ranges::end(range));
    }
    
    auto test() {
      std::vector<int> range {1,2,3,4,5};
      return std::move(range) | 
        std::ranges::views::transform([](auto x) { return x%2; });
    }
    
    int main() {
      auto y = to_unordered_set(test());
    }
    

    Demo