What is the advantage of using forwarding references in range-based for loops?

31,263

Solution 1

The only advantage I can see is when the sequence iterator returns a proxy reference and you need to operate on that reference in a non-const way. For example consider:

#include <vector>

int main()
{
    std::vector<bool> v(10);
    for (auto& e : v)
        e = true;
}

This doesn't compile because rvalue vector<bool>::reference returned from the iterator won't bind to a non-const lvalue reference. But this will work:

#include <vector>

int main()
{
    std::vector<bool> v(10);
    for (auto&& e : v)
        e = true;
}

All that being said, I wouldn't code this way unless you knew you needed to satisfy such a use case. I.e. I wouldn't do this gratuitously because it does cause people to wonder what you're up to. And if I did do it, it wouldn't hurt to include a comment as to why:

#include <vector>

int main()
{
    std::vector<bool> v(10);
    // using auto&& so that I can handle the rvalue reference
    //   returned for the vector<bool> case
    for (auto&& e : v)
        e = true;
}

Edit

This last case of mine should really be a template to make sense. If you know the loop is always handling a proxy reference, then auto would work as well as auto&&. But when the loop was sometimes handling non-proxy references and sometimes proxy-references, then I think auto&& would become the solution of choice.

Solution 2

Using auto&& or universal references with a range-based for-loop has the advantage that you captures what you get. For most kinds of iterators you'll probably get either a T& or a T const& for some type T. The interesting case is where dereferencing an iterator yields a temporary: C++ 2011 got relaxed requirements and iterators aren't necessarily required to yield an lvalue. The use of universal references matches the argument forwarding in std::for_each():

template <typename InIt, typename F>
F std::for_each(InIt it, InIt end, F f) {
    for (; it != end; ++it) {
        f(*it); // <---------------------- here
    }
    return f;
}

The function object f can treat T&, T const&, and T differently. Why should the body of a range-based for-loop be different? Of course, to actually take advantage of having deduced the type using universal references you'd need to pass them on correspondingly:

for (auto&& x: range) {
    f(std::forward<decltype(x)>(x));
}

Of course, using std::forward() means that you accept any returned values to be moved from. Whether objects like this makes much sense in non-template code I don't know (yet?). I can imagine that using universal references can offer more information to the compiler to do the Right Thing. In templated code it stays out of making any decision on what should happen with the objects.

Solution 3

I virtually always use auto&&. Why get bitten by an edge case when you don't have to? It's shorter to type too, and I simply find it more... transparent. When you use auto&& x, then you know that x is exactly *it, every time.

Share:
31,263
Ali
Author by

Ali

Updated on May 19, 2020

Comments

  • Ali
    Ali about 4 years

    const auto& would suffice if I want to perform read-only operations. However, I have bumped into

    for (auto&& e : v)  // v is non-const
    

    a couple of times recently. This makes me wonder:

    Is it possible that in some obscure corner cases there is some performance benefit in using forwarding references, compared to auto& or const auto&?

    (shared_ptr is a suspect for obscure corner cases)


    Update Two examples that I found in my favorites:

    Any disadvantage of using const reference when iterating over basic types?
    Can I easily iterate over the values of a map using a range-based for loop?

    Please concentrate on the question: why would I want to use auto&& in range-based for loops?

    • Lightness Races in Orbit
      Lightness Races in Orbit over 11 years
      Do you really see it "often"?
    • Lightness Races in Orbit
      Lightness Races in Orbit over 11 years
      I'm not sure there's enough context in your question for me to gauge how "crazy" it is where you're seeing it.
    • Ali
      Ali over 11 years
      @LightnessRacesinOrbit Long story short: why would I want to use auto&& in range-based for loops?
  • ildjarn
    ildjarn over 11 years
    On the other hand, there's no explicit disadvantage, is there? (Aside from potentially confusing people, which I don't think is much worth mentioning, personally.)
  • Lightness Races in Orbit
    Lightness Races in Orbit over 11 years
    That's an interesting point. Presumably there's no way of encouraging range-for to prefer T::const_iterator? I believe begin() and end() are used. Seems like a bit of an oversight, except that you can use rvalue refs. So yeah maybe this is the answer.
  • Howard Hinnant
    Howard Hinnant over 11 years
    Another term for writing code that unnecessarily confuses people is: writing confuscated code. It is best to make your code as simple as possible, but no simpler. This will help keep the bug count down. That being said, as && becomes more familiar, then maybe 5 years from now people will come to expect an auto&& idiom (assuming it actually does no harm). I don't know if that will happen or not. But simple is in the eye of the beholder, and if you're writing for more than just yourself, take your readers into account.
  • Howard Hinnant
    Howard Hinnant over 11 years
    I prefer const auto& when I want the compiler to help me check that I don't accidentally modify the elements in the sequence.
  • Xeo
    Xeo over 11 years
    @LightnessRacesinOrbit: template<class T> T const& as_const(T const& v){ return v; } and for(auto& e : as_const(v)) ....
  • Lightness Races in Orbit
    Lightness Races in Orbit over 11 years
    @HowardHinnant: Ah, but const T::iterator is not T::const_iterator, right? i.e. does that actually work in this case? I suppose that's a different question. Looks like "yes" but I don't know why.
  • Xeo
    Xeo over 11 years
    I personally like to use auto&& in generic code where I need to modify the elements of the sequence. If I don't, I'll just stick to auto const&.
  • Howard Hinnant
    Howard Hinnant over 11 years
    @Xeo: +1 It is because of enthusiasts like yourself, constantly experimenting and pushing for better ways of doing things, that C++ continues to evolve. Thank you. :-)
  • Luc Danton
    Luc Danton over 11 years
    @LightnessRacesinOrbit std::begin will delegate to calling member begin, which is overloaded on const.
  • Ali
    Ali over 11 years
    My problem is that your are giving up const-ness with auto&& if const auto& suffices. The question asks for the corner cases where I can get bitten. What are the corner cases which have not been mentioned by Dietmar or Howard yet?
  • Jonathan.
    Jonathan. almost 8 years
    Why isn't auto&& e an rvalue and therefore not possible to be assigned to? it's literally appearing on the left hand side here..
  • Howard Hinnant
    Howard Hinnant almost 8 years
    @Jonathan.: 7th paragraph of this section: open-std.org/jtc1/sc22/wg21/docs/papers/2002/… "Even though named rvalue references ..."
  • Jonathan.
    Jonathan. almost 8 years
    @HowardHinnant, thanks, i justed watched a presentation from Scott Meyers about this and it was immensely helpful with this and realising I'd misunderstood rvalue vs rvalue reference. I don't understand why universal references weren't actually a separate thing, part of the standard with a new token like &&&?
  • Howard Hinnant
    Howard Hinnant almost 8 years
    @Jonathan.: It was a judgment call made at the time: Kill two birds with one stone.
  • Dmitri Nesteruk
    Dmitri Nesteruk almost 8 years
    The top example compiles just fine in VS2015u2.
  • Howard Hinnant
    Howard Hinnant almost 8 years
    @DmitriNesteruk: With warnings on, it says it uses an extension to compile it. It is a dangerous extension whose main use is backwards compatibility with pre-C++98 code.
  • Dmitri Nesteruk
    Dmitri Nesteruk almost 8 years
    @HowardHinnant hold on, what would that extension actually do? cheat auto& into auto&& behind our back?
  • Howard Hinnant
    Howard Hinnant almost 8 years
    @DmitriNesteruk: I don't really know. Almost everything I know about VS, I glean from here: webcompiler.cloudapp.net
  • cyberbisson
    cyberbisson about 5 years
    Here's a way to get bitten if you do use auto&&. If the type you are capturing should be moved in the body of the loop (for instance), and is changed to a later date so it resolves to a const& type, your code will silently continue functioning, but your moves will be come copies. This code will be very deceptive. If, however, you explicitly specify the type as an r-value reference, whoever changed the container type will get a compilation error because you really really wanted these objects moved and not copied...
  • Jerry Ma
    Jerry Ma about 5 years
    @cyberbisson I just came across this: how can I enforce an rvalue reference without explicitly specify the type? Something like for (std::decay_t<decltype(*v.begin())>&& e: v)? I guess there is a better way ...
  • cyberbisson
    cyberbisson about 5 years
    @JerryMa It depends on what you do know about the type. As mentioned above, auto&& will give a "universal reference", so anything other than that will get you a more specific type. If v is assumed to be a vector, you could do decltype(v)::value_type&&, which is what I assume you wanted by taking the result of operator* on an iterator type. You could also do decltype(begin(v))::value_type&& to check the iterator's types instead of the container. If we have so little insight into the type, though, we might consider it a little clearer to just go with auto&&...
  • mrexodia
    mrexodia about 5 years
    "[...] then maybe 5 years from now people will come to expect an auto&& idiom [...]" 7 years later for(auto&& x : y) is still very confusing and definitely not an expected idiom.
  • Milan
    Milan about 3 years
    In the case of using a single ampersand (lvalue reference) in non-const way a range-based for loop, why does std::vector<bool> gives a compilation error but std::vector<int> does not?
  • Howard Hinnant
    Howard Hinnant about 3 years
    vector<bool> is not an array of bool, but is instead an array of bits made to look like an array of bool. When one gets a "reference" to a bit, one actually gets a class type proxy reference (returned by value). This rvalue proxy can not bind to a non-const lvalue reference. More info here: howardhinnant.github.io/onvectorbool.html
  • luca
    luca over 2 years
    Would It be possible to use "auto const&&" when read-only suffices and "auto&&" for modifying the values? Would that be a more general version of "auto const&" and "auto&"?
  • Howard Hinnant
    Howard Hinnant over 2 years
    auto const&& should work fine. However I don't see any advantage (or any significant difference) between that and auto const&.