Why aren't temporary container objects pipeable in range-v3?

264 Views Asked by At

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?

2

There are 2 best solutions below

5
On BEST ANSWER

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.

0
On

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.