8

Why is the following

#include <iostream>
#include <string>
#include <range/v3/all.hpp>

std::vector<int> some_ints() {
    return { 1,2,3,4,5 };
}

int main() {
    auto num_strings = some_ints() |
        ranges::views::transform([](int n) {return std::to_string(n); }) |
        ranges::to_vector;
    
    for (auto str : num_strings) {
        std::cout << str << "\n";
    }

    return 0;
}

an error, while

int main() {
    auto ints = some_ints();
    auto num_strings = ints |
        ranges::views::transform([](int n) {return std::to_string(n); }) |
        ranges::to_vector;

    for (auto str : num_strings) {
        std::cout << str << "\n";
    }

    return 0;
}

is fine?

I would expect the lifetime of the temporary to be extended to the lifetime of the whole pipeline expression so I don't understand what the problem is.

The error from Clang is

<source>:10:36: error: overload resolution selected deleted operator '|'
    auto num_strings = some_ints() |
                       ~~~~~~~~~~~ ^
/opt/compiler-explorer/libs/rangesv3/0.11.0/include/range/v3/view/view.hpp:153:13: note: candidate function [with Rng = std::vector<int, std::allocator<int>>, ViewFn = ranges::detail::bind_back_fn_<ranges::views::transform_base_fn, (lambda at <source>:11:34)>] has been explicitly deleted
            operator|(Rng &&, view_closure<ViewFn> const &)    // ****** READ THIS *******

from Visual Studio I get

error C2280: 'std::vector<int,std::allocator<int>> ranges::views::view_closure_base_ns::operator |<std::vector<int,std::allocator<int>>,ranges::detail::bind_back_fn_<ranges::views::transform_base_fn,main::<lambda_1>>>(Rng &&,const ranges::views::view_closure<ranges::detail::bind_back_fn_<ranges::views::transform_base_fn,main::<lambda_1>>> &)': attempting to reference a deleted function
1>        with
1>        [
1>            Rng=std::vector<int,std::allocator<int>>
1>        ]

Both errors seem to be saying that the pipe operator is explicitly deleted for r-value references?

jwezorek
  • 8,592
  • 1
  • 29
  • 46

2 Answers2

8

Short answer would be because they are lazy and | does not transfer ownership.

I would expect the lifetime of the temporary to be extended to the lifetime of the whole pipeline expression so I don't understand what the problem is.

Yes, that is exactly what would happen, but nothing more. Meaning that as soon as the code hits ;, some_ints() dies and num_strings now contains "dangling" range. So a choice was made to forbid this example to compile.

To expand on my comment, the issue is actually within operator| which does not take rvalue(~temporary) on its left side and transform on the right. It does not because it cannot know in advance whether the result will be iterated immediately while the temporary is still alive (that is your case) or whether you will just store the range and iterate over it later. For the latter, the iteration would happen with a dangling reference to the now-dead temporary.

Yes, there might have been some "look-ahead overload magic" to see whether you iterate the range and store it somewhere in a single expression, but it might be brittle or not worth the complexity. The sane decision was to forbid this at compile-time as the runtime errors from this might have been too common and hard to debug.

Quimby
  • 17,735
  • 4
  • 35
  • 55
  • 1
    Wait ... actually I dont understand this. `num_strings` is a vector of strings. What could be dangling? i.e. doesnt `ranges::to_vector` consume the lazy range and make a concrete vector when the pipeline expression executes? – jwezorek May 05 '22 at 18:52
  • 1
    @jwezorek • I suspect it is because the **entire** pipeline expression can't be determined *a priori* if it is going to be fully evaluated in the statement, or lazily evaluated in a different statement. – Eljay May 05 '22 at 18:57
  • 1
    yeah, it must be that the library explicitly disallows piping from temporaries because that will *often* lead to dangling range views but in this case it wouldnt. – jwezorek May 05 '22 at 19:00
  • 1
    @jwezorek - There is no language mechanism for the library to *know* that in *this case* it wouldn't. – StoryTeller - Unslander Monica May 05 '22 at 19:02
  • @jwezorek Yes, in this case it would not but the error is caused because there is no `operator|` which accepts rvalues at left and `transform` at right exactly because `transform` would have no way of knowing whether you will eventually store the result in some same container. So the overload is deleted. I suppose there could be some magic return types, similarly to math libraries doing fancy fusion of matrix operations, but there is no such mechanism right now. I am not sure whether it would be possible to do. – Quimby May 05 '22 at 19:20
1

If you're certain it'd be evaluate in time you can simply convert it to lvalue reference.

#include <iostream>
#include <string>
#include <range/v3/all.hpp>

struct to_lvalue_t{} to_lvalue, enable_unsafe_range; // whatever you want name it

template<typename T>
T& operator |(T&& v,to_lvalue_t){return v;}

std::vector<int> some_ints() {
    return { 1,2,3,4,5 };
}
int main() {
    auto num_strings = some_ints() | enable_unsafe_range |
        ranges::views::transform([](int n) {return std::to_string(n); }) |
        ranges::to_vector;

    for (auto str : num_strings) {
        std::cout << str << "\n";
    }
}

https://godbolt.org/z/dbz1v9Gzo



For the reason, range-v3 probably decide it's dangerous the have potentially dangling reference. (because the execution would usually be deferred)


For example, the second code is actually good without to_vector.

int main() {
    auto ints = some_ints();
    auto num_strings = ints |
        ranges::views::transform([](int n) {return std::to_string(n); });
        // ranges::to_vector;

    for (auto str : num_strings) {
        std::cout << str << "\n";
    }

    return 0;
}

But the first code would not work without it.


Fwiw, it's actually possible to detect a unevaluated rvalue-based range. if you explicitly type the return type and the constructor do the check (possibly on type).

But with auto&& (or auto with guaranteed copy elision), there is not much you can to.

apple apple
  • 10,292
  • 2
  • 16
  • 36