23

I used the following syntactic sugar:

for (auto& numberString: {"one", "two", "three", "four"}) { /* ... */}

Is this valid code? AFAIK, based on this question, this should be illegal, yet the code runs as expected. I don't think my understanding is correct on the matter.

As far as I know, only literals should not have memory addresses, yet the linked question is talking about temporaries and r-values.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Dávid Tóth
  • 2,788
  • 1
  • 21
  • 46
  • 8
    The temporary `std::initializer_list` object will actually have a life-time that encompasses the loop, so it's valid. – Some programmer dude Mar 07 '23 at 08:00
  • So if my understanding is correct: as long as the lifetime of a temporary is valid, taking a reference from it is valid behavior. In the referenced question, there is no connection to String literals, only the lifetime of the temporary is relevant. Am I understaning it correctly? – Dávid Tóth Mar 07 '23 at 08:16
  • 5
    Yes it's the lifetime of the temporary that matters. As long as the lifetime of the temporary is longer than the lifetime of the reference, all is good and well. – Some programmer dude Mar 07 '23 at 08:20

1 Answers1

28

Yes, this code is valid.

Keep in mind that (for C++17), the compiler will semantically replace the range-based for loop by the construct

{

    auto && __range = {"one", "two", "three", "four"};
    for (auto __begin = begin(__range), __end = end(__range); __begin != __end; ++__begin)
    {

        auto& numberString = *__begin;
        /* ... */
    }

}

You see, the lifetime of the initializer_list is extended to the lifetime of __range inside of the outermost scope in the replacement.

Note however that you still can easily cause undefined behavior if the range expression contains a temporary itself:

struct some {
   auto get_list() { return {"one", "two", "three", "four"}; }
};

some foo() { return some{ }; }

for(auto& numberString : foo().get_list()) { /* ... */ }

The above code will result in a dangling reference in <= C++20. Only in C++23, the lifetime of the temporary created by foo() will get extended such that it becomes valid. See also https://en.cppreference.com/w/cpp/language/range-for

Jodocus
  • 7,493
  • 1
  • 29
  • 45
  • 5
    I'm pretty sure your second example would only be undefined if `get_list` returned a reference to a member of `some`. As-is, `foo` returns a temporary `some` object which lives until the end of the full expression, which is long enough to call `get_list` on it. `get_list` returns a temporary object which then gets lifetime-extended per the usual rules. – Miles Budnek Mar 07 '23 at 20:23
  • 1
    Actually I think the second example is undefined even in C++20. The backing buffer of the initializer_list is local to `get_list`, and no lifetime extension of the initializer_list return value will change that. (GCC and Clang both warn about returning the address of a temporary once I change the return type to `std::initializer_list`) – Sebastian Redl Mar 08 '23 at 08:58