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

Viewed 103

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 Answers

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.

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.

Related