How can I test whether a type is a range from which I can move elements?

Viewed 78

Assume that I have a function template that can accept a range of some type T. I want to check whether it's safe to move from that range.

For example, if a function accepts an rvalue reference to a certain value, it is safe to move from it. If, for example, that function may accept both rvalue and lvalue references, we can use std::forward to conditionally move it if necessary.

Now I want to achieve the same thing, but for ranges. Initially I tried using the following concept:

template <typename Range>
concept movable_source = std::ranges::range<Range> &&
        !std::ranges::borrowed_range<Range> &&
        (!std::is_lvalue_reference_v<Range>);

and it worked okay. It reported true for std::vector<T>&&, false for std::vector<T>& and true again for std::vector<T>.

Usage:

template <typename T>
auto print_movability(T&& t) -> void {
    std::cout << movable_source<T> << ' ';
}

auto main() -> int {
    auto vec = std::vector<int>();
    print_movability(vec);
    print_movability(std::move(vec));
    print_movability(std::vector<int>());
}

This correctly prints 0 1 1.

However, the following snippet breaks this logic:

print_movability(vec | std::views::transform([](int& e) -> int& { return e; }));

This reports 1 since the resulting view is neither a borrowed range nor is it an lvalue reference.

I could of course simply add another check to make sure that the type isn't a view, but would that be enough? What would be the correct way to test whether a type is a range whose elements we can move?

1 Answers

I would suggest making it up to the caller to tell you this by using a range adaptor that produces rvalue references (views::move in range-v3, views::as_rvalue proposed for C++23). That way you don't have to guess, since you'll get a range of rvalues:

algo(vec); // can't move
algo(vec | views::move); // can move
algo(vec | views::transform(f)); // can't move
algo(vec | views::transform(f) | views::move); // can move

Calling this "can move" isn't really meaningful, since we actually get is a range of rvalues, so of course you can move. That's just what you get.


As to guessing... that's pretty hard. Borrowed-ness isn't relevant, so we can skip that part. If the range is an lvalue, pretty sure we can't move. If the range is an rvalue but is a view, we probably can't move (we'll typically get views as rvalues) except for some specific views that we can. So an approximation could be:

template <class R>
inline constexpr bool move_safe_range = !view<R>;

template <class R>
inline constexpr bool move_safe_range<owning_view<R>> = true;

template <class T>
inline constexpr bool move_safe_range<single_view<T>> = true;

template <class R>
concept movable_source =
    input_range<R>
    && (!is_lvalue_reference_v<R>)
    && move_safe_range<R>;

But I'm not sure this is a great approach, cause even an rvalue view could be move-safe, for instance if we're views::transform-ing an owning_view? The views::move/views::as_rvalue is more reliable (even as it requires more annotation).

Related