4

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

dodomorandi
  • 1,143
  • 1
  • 11
  • 18
  • 1
    What's the use case for this? – Eyal K. Sep 12 '17 at 08:54
  • shouldn't you use `std::forward` instead of `std::move` in your loop? – W.F. Sep 12 '17 at 08:56
  • @EyalK. this problem came into my mind when writing a multithread for-loop wrapper, but I think that it could be applicable in many contexts. – dodomorandi Sep 12 '17 at 09:06
  • @W.F. Yes indeed, it was just to make more clear the specific request. Inside the generic function, `std::forward` is expected to be used – dodomorandi Sep 12 '17 at 09:07
  • range-based for loop with universal reference (`for (auto &&val : cont)`) is not resolved as rvalue reference. Have a look into ranges-v3: [ranges::move](https://ericniebler.github.io/range-v3/group__group-algorithms.html#gad3830f80a601a6a46958ca50ef2e34c5). Maybe, you should/can implement forwarding analogue of it. I believe functionality you described is only necessary for those use cases. – Andrei R. Sep 12 '17 at 09:20
  • Could you please clarify how your proposed change would help: In the body of your last loop example, `el` is still an lvalue, and you need `std::move(el)`, regardless of the nature of the container. So that situation seems no different from what we have with the present semantics. – Kerrek SB Sep 12 '17 at 09:50
  • 1
    Could you also please add an actual proposed specification for this new kind of loop? It's hard to evaluate the idea otherwise. – Kerrek SB Sep 12 '17 at 09:52
  • @KerrekSB I added a short snippet to describe a simple use case. I can try to write something more *formal*, but it would be the first time I write a proposal. Probably there are people much more prepared than me to do that, but I can try. – dodomorandi Sep 12 '17 at 10:10
  • btw, `std::array` has no constructor accepting begin/end pair – Andrei R. Sep 12 '17 at 11:13
  • @AndreiR. There is no reason you could not create a std::array, std::array or std::array, 2> (if the typenames exist), so I don't see your point. – dodomorandi Sep 12 '17 at 11:22
  • 1
    To rephrase @KerrekSB's question, [this](http://en.cppreference.com/w/cpp/language/range-for#Explanation) is what range-based `for` currently expands to. What part of it do you want to change, and how? – T.C. Sep 12 '17 at 11:23

4 Answers4

6

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));
}
T.C.
  • 133,968
  • 17
  • 288
  • 421
  • Obviously, there are different kind of *iterable* things, and some of them handle non-owning ranges. However, we still have *std::make_move_iterator*, and you have to use it carefully. An example [like this](https://wandbox.org/permlink/B0lBfYKuIsDgQo4M) works flawlessly, but if you change the *std::vector* to some non-owning container, it won't work as expected. Introducing the syntax I am talking about only makes life easier... if it doesn't break existing code. – dodomorandi Sep 12 '17 at 10:48
  • 2
    @dodomorandi There's only one range-based `for` loop, and it needs to work correctly for all kinds of iterables - including nonowning ones which occur quite frequently. It knows the value category of the iterable, but without knowing if the iterable is owning or not - and it has no way of knowing that - what it can safely do with the value category information is extremely limited. – T.C. Sep 12 '17 at 11:58
2

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)))
{
    // ...
}
Jarod42
  • 203,559
  • 14
  • 181
  • 302
  • 1
    I know that this could be a way to handle the situation, but (sorry to say) it was not my question. I am explicitly talking about the possible issues of adding the syntax of *forwarding range-based loop* to the language. – dodomorandi Sep 12 '17 at 09:22
  • @dodomorandi I was in the middle of posting a similar solution when Jarod put this up. The problem with assuming move-iterators from a moved container would be that it is not clear whether you would want to move the entire container or just the elements. There's not enough information at the call site to know. Jarod's solution makes it explicit. – Richard Hodges Sep 12 '17 at 10:35
  • @dodomorandi the language already allows you to write `for(auto&& el : std::forward(iterable))`. You will need to add ref-qualifiers to your `begin` and `end` member functions to return a `move_iterator` instead. – Simple Sep 12 '17 at 10:35
  • @Simple I know that I can, but I don't see the difference betweeen passing a forwarded iterable between the reference to iterable. In both cases you will get lvalue references to the elements, therefore you will never use the forwarding syntax for range-based loops. – dodomorandi Sep 12 '17 at 10:38
  • @RichardHodges My objection is always about the use of *std::move* or *std::forward* as *argument* of a range-based loop. At the actual state, it does not make any difference, therefore there is no reason to use it. – dodomorandi Sep 12 '17 at 10:53
2

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

Andrei R.
  • 2,374
  • 1
  • 13
  • 27
  • In your example there is no reason to use the *move* function, because it does not change the behaviour of the code. Why would you have to use it in this way, with the current standard? – dodomorandi Sep 12 '17 at 10:17
  • 1
    @dodomorandi, code like this can exist and your change can possibly break it. That's why it won't be accepted into standard. – Andrei R. Sep 12 '17 at 11:11
2

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.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524