Moving in range-based loop in generic C++ code?

2.8k views Asked by At

Imagine that you have this generic pseudo-code:

template<typename Iterable>
void f(Iterable&& iterable)
{
   ...
}

We want to handle rvalue and lvalue references to iterable objects1, and the idea is that the function handles the container performing operations element by element.

It is plausible that we want to forward the reference specification of the container to the elements. In other words, if iterable is an rvalue reference, the function will have to move the elements from the container.

Using C++17, I would do

auto [begin, end] = [&] {
    if constexpr(std::is_lvalue_reference_v<Iterable>)
        return std::array{std::begin(iterable),
                          std::end(iterable)};
    else
        return std::array{
            std::make_move_iterator(std::begin(iterable)),
            std::make_move_iterator(std::end(iterable))};
}();
std::for_each(begin, end, [&](auto&& element)
{
    ...
});

Obviously, this is not the best code to maintain2, error prone and probably not so easy to optimize for the compiler.

My question is: it could be possible, for future C++ standards, to introduce the concept of forwarding range-based loops? It would be nice if this

for(auto&& el : std::move(iterable))
{
    ...
}

could handle el as rvalue reference. In this way, this would be possible:

template<typename Iterable>
void f(Iterable&& iterable)
{
    for(auto&& el : std::forward<Iterable>(iterable))
    {
        /*
         *  el is forwarded as lvalue reference if Iterable is lvalue reference,
         *  as rvalue reference if Iterable is rvalue reference
         */
        external_fun(std::forward<decltype(el)>(el));
    }
}

I am concerned about code-breaking changes, but at the same time I am not able to think about situations in which passing a rvalue reference as argument of a range based loop is expected to work without moving objects.

As suggested, I tried to write down how I would change the 6.5.4 section of the standard. The draft can be read at this address.

Do you think that it would be possible to introduce this feature without introducing serious issues?

1Checked with C++20 concepts or static_asserts
2And it's quite worse without C++17

4

There are 4 answers

2
T.C. On

This won't work. Fundamentally there are two kinds of things you can iterate over: those that own the elements, and those that don't. For non-owning ranges, the value category of the range is immaterial. They don't own their elements and so you can't safely move from them. The range-based for loop must work with both kind of ranges.

There are also corner cases to consider (e.g., proxy iterators). The range-based for loop is basically syntax sugar that imposes only a very minimal set of requirements on the thing being iterated over. The benefit is that it can iterate over lots of things. The cost is that it doesn't have much room to be clever.


If you know that the iterable in fact owns its elements (so that moving is safe), then all you need is a function that forwards something according to the value category of some other thing:

namespace detail {
    template<class T, class U>
    using forwarded_type = std::conditional_t<std::is_lvalue_reference<T>::value,
                                              std::remove_reference_t<U>&, 
                                              std::remove_reference_t<U>&&>;
}
template<class T, class U>
detail::forwarded_type<T,U> forward_like(U&& u) {
    return std::forward<detail::forwarded_type<T,U>>(std::forward<U>(u));
}
2
Andrei R. On

your suggestion will introduce breaking changes. Assume this piece of code:

vector<unique_ptr<int>> vec;
for (int i = 0; i < 10; ++i)
    vec.push_back(make_unique<int>(rand()%10));

for (int i = 0; i < 2; ++i) {
    for (auto &&ptr : move(vec))
        cout << (ptr ? *ptr : 0) << " ";
    cout << endl;
}

With current standard, it'll print two same lines

5
Jarod42 On

You may add a wrapper, something like:

template <typename T> struct ForwardIterable;

template <typename T> struct ForwardIterable<T&&>
{
    ForwardIterable(T&& t) : t(t) {}
    auto begin() && { return std::make_move_iterator(std::begin(t)); }
    auto end() && { return std::make_move_iterator(std::end(t)); }

    T& t;
};

template <typename T> struct ForwardIterable<T&>
{
    ForwardIterable(T& t) : t(t) {}
    auto begin() { return std::begin(t); }
    auto end() { return std::end(t); }
    auto begin() const { return std::begin(t); }
    auto end() const { return std::end(t); }

    T& t;
};

template <typename T>
ForwardIterable<T&&> makeForwardIterable(T&& t)
{
    return {std::forward<T>(t)};
}

And then

for(auto&& el : makeForwardIterable(std::forward(iterable)))
{
    // ...
}
0
Yakk - Adam Nevraumont On

Write a simple range type. It stores two iterators and exposes begin() and end().

Write a move_range_from(Container&&) function that returns a range of move iterators.

Write move_range_from_if<bool>(Container&&) that creates a range or moves from the range conditionally.

Support lifetime extension in both.

template<typename Iterable>
void f(Iterable&& iterable) {
  auto move_from = std::is_rvalue_reference<Iterable&&>{};
  for(auto&& e: move_range_from_if< move_from >(iterable) ) {
  }
}

does what you want.

This supports both ranges (non-owning) and containers and doesn't require a language extension. And it doesn't break existing code.

The lifetime extension feature is so you can call these functions with prvalues; without it, for(:) loops don't lifetime extend arguments to the loop target function call.