在基于范围的for循环中,const auto引用的生命周期是否延伸到作用域的末尾?

3

我发现一段代码,它在一个基于范围的for循环中清除了一个deque。尽管如此,const auto引用仍然被随后使用。以下是一个简单的复制:

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;

这个问题似乎已经将 ref 的值获取了。但我认为 q1.clear() 应该删除了 ref 的底层数据。

  1. 这个引用的生命周期是否持续到作用域,还是未定义行为?
  2. 如果这是未定义行为,那么为什么它能够工作?

编辑:错过了原始代码中的断点,添加回来以缩小讨论范围。


5
未定义行为并不意味着代码不能“正确”地运行。有效的输出是所有可能结果集合中的一部分。这就是为什么未定义行为如此危险。代码在一台机器上可能正常工作,在另一台机器上则不然。 - NathanOliver
if语句的内容未被执行。q1.front().x为0。 - sweenish
引用没有生命周期,它所指向的对象有。当该对象的生命周期结束时,引用将变得无效。通过绑定到const引用进行的生命周期延长仅适用于临时对象。 - molbdnilo
1个回答

8
引用的生命周期是否贯穿作用域,还是这是一种未定义的行为?首先,考虑以下基于范围的for循环:
for (auto const& ref : q1) {
  // ...
}

根据 [stmt.ranged]/1,扩展为
{
  auto &&range = (q1);
  for (auto begin_ = range.begin(), end_ = range.end(); begin_ != end_;
       ++begin_) {
    auto const& ref = *begin_;
    // ...
  }
}

根据std::deque<T>::clear的文档,现在如下所示:

从容器中删除所有元素。调用此函数后,size() 返回零。

使任何引用、指针或迭代器无效,这些引用、指针或迭代器指向包含的元素。 任何超过末尾的迭代器也将无效。

如果进入了if (ref.x == 1)分支,则在q1.clear();之后,指向对象ref(这里是deque元素)的迭代器将无效,并且任何进一步使用该迭代器的操作都是未定义的行为,例如在下一个循环迭代中对其进行递增和取消引用。
现在,在这个特定的例子中,由于for-range-declarationauto const&而不是只有auto &如果*begin_解引用(在使迭代器失效之前)解析为一个值而不是一个引用,则此(临时)值将绑定到ref引用并扩展其生命周期。然而,std::deque是一个序列容器,应遵守[sequence.reqmts]的要求,其中特别是表78指定:
在表格77和78中,X表示一个序列容器类,a表示类型为X的值[...]。
表格78:
表达式:a.front() 返回类型:reference;(对于常量aconst_­reference
操作语义:*a.begin() 虽然不是完全无懈可击的,但似乎很有可能对std::deque迭代器进行解引用(例如begin()end())会产生引用类型,这是Clang和GCC都同意的事情:
#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.   
}

在这种情况下,使用ref
 q2.push_front(ref);
q1.clear() 之后,UB(未定义行为)可能会发生。
我们还可以注意到,存储为 end_end() 迭代器在调用 clear() 后也会失效,这是扩展循环不变式在下一次迭代中测试时的另一个 UB 源。
如果这是未定义的行为,为什么它能够正常工作?试图理解未定义行为是徒劳无功的。

2
@mch 这里的 q1 可能代表一个带有副作用的表达式。因此,您只想评估该表达式一次。 - François Andrieux
1
@mch - 你可以为范围设置任意表达式,甚至可以生成临时对象的表达式。它需要被扩展。 - StoryTeller - Unslander Monica
@RichardCritten 不错的观点,特别是这里的_for-range-declaration_是auto const&而不仅仅是auto&:我在这个讨论中进行了扩展回答。 - dfrib
1
@RichardCritten - LegacyBidirectionalIterator 指定 *a--std::iterator_traits<It>::reference。在这种情况下,确实 是一个适当的引用。这些命名要求旨在“包含”彼此。多通行保证意味着我们可以进一步指定 *it 的作用。 - StoryTeller - Unslander Monica
根据 https://timsong-cpp.github.io/cppwp/n4861/sequence.reqmts#3,`deque` 的迭代器还需满足 Cpp17InputIterator 的要求。而根据 https://timsong-cpp.github.io/cppwp/n4861/input.iterators#tab:inputiterator,Cpp17InputIterator 要求 *it 的返回类型为 reference - dfrib
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接