I have ran across a bit of code, that clears a deque while inside a range-based for loop. The const auto reference is still used subsequently. Here is a small reproduction.
struct Foo
{
int x;
}
deque<Foo> q1;
deque<Foo> q2;
q1.push_front({0});
q1.push_front({1});
for (const auto& ref : q1)
{
if (ref.x == 1)
{
q1.clear();
q2.push_front(ref);
break;
}
}
std::cout << q2.front().x << std::endl;
This q2 appears to have gotten the value of ref. But I am thinking that the q1.clear()
should have deleted the underlying data of ref
.
- Does the lifetime for the reference last through the scope, or is this an undefined behaviour?
- If it is an undefined behaviour, why is this working???
edit: missed the break from the original code, added back to narrow down the discussion.
CodePudding user response:
Does the lifetime for the reference last through the scope, or is this an undefined behaviour?
To start with, the following range-based for loop:
for (auto const& ref : q1) {
// ...
}
expands, as per [stmt.ranged]/1, to
{
auto &&range = (q1);
for (auto begin_ = range.begin(), end_ = range.end(); begin_ != end_;
begin_) {
auto const& ref = *begin_;
// ...
}
}
Now, as per the docs for std::deque<T>::clear
:
Erases all elements from the container. After this call, size() returns zero.
Invalidates any references, pointers, or iterators referring to contained elements. Any past-the-end iterators are also invalidated.
if the if (ref.x == 1)
branch is ever entered, then past the point of q1.clear();
the iterator whose pointed-to object ref
is referring to (here: the deque element) is invalidated, and any further use of the iterator is undefined behavior: e.g. incrementing it an dereferencing it in the next loop iteration.
Now, in this particular example, as the for-range-declaration is auto const&
as compared to only auto &
, if the *begin_
dereferencing (prior to invalidating iterators) resolves to a value and not a reference, this (temporary) value would bind to the ref
reference and have its lifetime extended. However, std::deque
is a sequence container and shall adhere to the requirements of [sequence.reqmts], where particularly Table 78 specifies:
/3 In Tables 77 and 78, X denotes a sequence container class, a denotes a value of type X [...]
Table 78:
- Expression:
a.front()
- Return type:
reference;
(const_reference
for constanta
)- Operational semantics:
*a.begin()
Whilst not entirely water-proof, it seems highly likely that dereferencing a std::deque
iterator (e.g. begin()
or end()
) yields a reference type, something both Clang and GCC agrees on:
#include <deque>
#include <type_traits>
struct S{};
int main() {
std::deque<S> d;
static_assert(std::is_same_v<decltype(*d.begin()), S&>, "");
// Accepted by both clang and gcc.
}
in which case the use of ref
in
q2.push_front(ref);
is UB after the q1.clear()
.
We may also note that the end()
iterator, stored as end_
, is also invalidated by the invocation of clear()
, which is another source of UB once the invariant of the expanded loop is tested in the next iteration.
If it is an undefined behaviour, why is this working???
Trying to reason with undefined behavior is a futile exercise.