What is the advantage of using forwarding references in range-based for loops?
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.
Ali
Updated on May 19, 2020Comments
-
Ali about 4 years
const auto&
would suffice if I want to perform read-only operations. However, I have bumped intofor (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&
orconst 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 over 11 yearsDo you really see it "often"?
-
Lightness Races in Orbit over 11 yearsI'm not sure there's enough context in your question for me to gauge how "crazy" it is where you're seeing it.
-
Ali over 11 years@LightnessRacesinOrbit Long story short: why would I want to use
auto&&
in range-based for loops?
-
-
ildjarn over 11 yearsOn 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 over 11 yearsThat's an interesting point. Presumably there's no way of encouraging range-
for
to preferT::const_iterator
? I believebegin()
andend()
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 over 11 yearsAnother 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 over 11 yearsI prefer
const auto&
when I want the compiler to help me check that I don't accidentally modify the elements in the sequence. -
Xeo over 11 years@LightnessRacesinOrbit:
template<class T> T const& as_const(T const& v){ return v; }
andfor(auto& e : as_const(v)) ...
. -
Lightness Races in Orbit over 11 years@HowardHinnant: Ah, but
const T::iterator
is notT::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 over 11 yearsI 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 toauto const&
. -
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 over 11 years@LightnessRacesinOrbit
std::begin
will delegate to calling memberbegin
, which is overloaded onconst
. -
Ali over 11 yearsMy problem is that your are giving up
const
-ness withauto&&
ifconst 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. almost 8 yearsWhy 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 almost 8 years@Jonathan.: 7th paragraph of this section: open-std.org/jtc1/sc22/wg21/docs/papers/2002/… "Even though named rvalue references ..."
-
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 almost 8 years@Jonathan.: It was a judgment call made at the time: Kill two birds with one stone.
-
Dmitri Nesteruk almost 8 yearsThe top example compiles just fine in VS2015u2.
-
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 almost 8 years@HowardHinnant hold on, what would that extension actually do? cheat
auto&
intoauto&&
behind our back? -
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 about 5 yearsHere'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 aconst&
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 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 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. Ifv
is assumed to be avector
, you could dodecltype(v)::value_type&&
, which is what I assume you wanted by taking the result ofoperator*
on an iterator type. You could also dodecltype(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 withauto&&
... -
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 about 3 yearsIn the case of using a single ampersand (lvalue reference) in
non-const
way a range-based for loop, why doesstd::vector<bool>
gives a compilation error butstd::vector<int>
does not? -
Howard Hinnant about 3 years
vector<bool>
is not an array ofbool
, but is instead an array of bits made to look like an array ofbool
. 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 over 2 yearsWould 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 over 2 years
auto const&&
should work fine. However I don't see any advantage (or any significant difference) between that andauto const&
.