36

I find myself using C++11 more and more lately, and where I would have been using iterators in the past, I now am using range-based for loops whenever possible:

std::vector<int> coll(10);
std::generate(coll.begin(), coll.end(), []() { return rand(); } );

C++03:

for (std::vector<int>::const_iterator it = coll.begin(); it != coll.end(); ++it) {
   foo_func(*it);
}

C++11:

for (auto e : coll) { foo_func(e); }

But what if the collection element type is a template parameter? foo_func() probably will be overloaded to pass complex (= expensive to copy) types by const reference, and simple ones by value:

foo_func(const BigType& e) { ... };
foo_func(int e) { ... };

I didn't give this much thought while I was using the the C++03-style code above. I would iterate the same way, and since dereferencing a const_iterator produces a const reference, everything was fine. But using the C++11 range-based for loop, I need to use a const reference loop variable to obtain the same behavior:

for (const auto& e : coll) { foo_func(e); }

And suddenly I wasn't sure anymore, if this wouldn't introduce unnecessary assembly instructions if auto was a simple type (such as a behind-the-scene pointer to implement the reference).

But compiling a sample application confirmed that there is no overhead for simple types, and that this seems to be the generic way to use range-based for loops in templates. If this hadn't been the case, boost::call_traits::param_type would have been the way to go.

Question: Are there any guarantees in the standard?

(I realize that the issue is not really related to range-based for loops. It's also present when using const_iterators.)

Daniel Gehriger
  • 7,339
  • 2
  • 34
  • 55

2 Answers2

16

The standard containers all return references from their iterator (note, however, that some "containers aren't really container, e.g., std::vector<bool> which returns a proxy). Other iterators might return proxies or values although this isn't strictly supported.

Of course, the standard doesn't make any guarantees with respect to performance. Any sort of performance related feature (beyond complexity guarantees) are considered to be quality of implementation.

That said, you might want to consider having the compiler make the choice for you as it did before:

for (auto&& e: coll) { f(e); }

The main issue here is that f() may receive a non-const reference. This can be prevented if necessary using a const version of coll.

Dietmar Kühl
  • 150,225
  • 13
  • 225
  • 380
  • 2
    Or... just using `auto const&`. ;) – Xeo Oct 24 '12 at 21:14
  • 1
    @Xeo: If the iterator yields a value rather than a [`const`] reference to the value the notation `T&&` deduces the value rather than a reference to the value. – Dietmar Kühl Oct 24 '12 at 21:17
  • *Huh?* `auto&&` will always be a reference, no matter what the iterators yield. If it does indeed yield a value, it will just be an rvalue reference. (Note: I may have misunderstood your comment.) – Xeo Oct 24 '12 at 21:18
  • 1
    @DietmarKühl: Interesting point about `auto&&`. I'm at relative easy with rvalue references in the context of move semantics and perfect forwarding; but I haven't thought about using them in this case. How about `for (const auto&& e : coll) { ... }` then? – Daniel Gehriger Oct 24 '12 at 21:41
  • 1
    @Xeo: Well, right, the type of `e` isn't a value type but `T&&` for some type `T`. When using `auto const& e` the type of `e` is `T const&`. To really take advantage of the difference, it is probably needed to use `f(std::forward(e))` which isn't really pretty. – Dietmar Kühl Oct 24 '12 at 22:37
  • 2
    @Daniel: `auto const&&` will not be a universal reference and will only bind to rvalues. – Xeo Oct 24 '12 at 23:03
11

6.5.4/1 says:

for ( for-range-declaration : braced-init-list ) statement

let range-init be equivalent to the braced-init-list. In each case, a range-based for statement is equivalent to

{
    auto && __range = range-init;
    for ( auto __begin = begin-expr,
                __end = end-expr;
            __begin != __end;
            ++__begin ) {
        for-range-declaration = *__begin;
        statement
    }
}

(further explanation follows of the meanings of all that __ gubbins).

The standard doesn't make any guarantees whether that line const auto &e = *__begin introduces a performance overhead, of course, compared with directly using *__begin instead of e inside statement. Implementations are permitted to implement references by laboriously copying a pointer into some stack slot and then reading it back each time the reference is used, and are not required to optimize.

But there's no reason why there should be an overhead in a sensible compiler, in the case where __begin is a container iterator (whose operator* returns a reference), and then e is passed by value in statement.

Steve Jessop
  • 273,490
  • 39
  • 460
  • 699
  • Thanks Steve. So how would you write a generic range-based for loop? – Daniel Gehriger Oct 24 '12 at 21:26
  • @Daniel: I think `for (const auto& e : coll) { foo_func(e); }` is fine, but I haven't used C++11 enough to claim to be able to write a style guide for myself that dictates it. The use for `boost::call_traits::param_type` is that once you're passing a parameter, then there's a potential overhead in using a reference rather than a small object type, because there's a calling convention restricting things. Within a function I think I trust the compiler to treat reference variables as "pure" aliases. If `*__begin` returns by value I wouldn't bet on avoiding an extra temporary without checking it. – Steve Jessop Oct 24 '12 at 21:34
  • I wonder why someone has named iterator `__begin` instead of something like `__it`. Incrementing `__begin` looks really bad... – mip Aug 15 '22 at 15:22
  • @mip: I don't particularly feel the need to defend the practice, but if I had to I think probably `__begin` is supposed to represent the beginning of the work that's left to do, as opposed to the beginning of the work we originally needed to do. In functional programming you might have a tail-recursive function with `begin` and `end` parameters, and then it would make a lot of sense: this is some kind of imperative dual to that design :-) – Steve Jessop Nov 23 '22 at 12:07