基于范围的for循环在临时范围上

alf*_*lfC 7 c++ temporary-objects c++11 c++20 for-range

感谢valgrind中的一些分段错误和警告,我发现这段代码不正确,并且在for-range循环中有一些悬空引用.

#include<numeric>
#include<vector>

auto f(){
    std::vector<std::vector<double>> v(10, std::vector<double>(3));
    iota(v[5].begin(), v[5].end(), 0);
    return v;
}

int main(){
    for(auto e : f()[5])
        std::cout << e << std::endl;
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

它看起来好像beginend从临时取出,并循环丢失.

当然,一种方法是做

    auto r = f()[5];
    for(auto e : r)
        std::cout << e << std::endl;
Run Code Online (Sandbox Code Playgroud)

但是,我想知道为什么for(auto e : f()[5])是一个错误,如果有更好的方法或某种方式设计f或甚至容器(std::vector)以避免这个陷阱.

随着迭代循环是比较明显的,为什么这个问题发生(beginend来自不同的临时对象)

for(auto it = f()[5].begin(); it != f()[5].end(); ++it)
Run Code Online (Sandbox Code Playgroud)

但是在for-range循环中,就像在第一个例子中一样,这个错误似乎很容易.

Bar*_*rry 8

我想知道为什么for(auto e : f()[5])是错误

我只回答这部分。原因是基于范围的 for 语句只是语法糖,大约:

{
    auto&& __range = f()[5]; // (*)
    auto __begin = __range.begin(); // not exactly, but close enough
    auto __end = __range.end();     // in C++17, these types can be different
    for (; __begin != __end; ++__begin) {
        auto e = *__begin;
        // rest of body
    }
}
Run Code Online (Sandbox Code Playgroud)

看看第一行。发生什么了?operator[]on avector返回对该对象的引用,因此__range绑定到该内部引用。但是,临时文件在行尾超出范围,破坏了其所有内部结构,并__range立即成为悬空引用。这里没有生命周期延长,我们永远不会将引用绑定到临时对象。

在更正常的情况下,for(auto e : f())我们会直接绑定__rangef()即将引用绑定到临时对象,这样临时对象的生命周期就会延长到引用的生命周期,这就是完整的for语句。

为了增加更多的皱纹,在其他情况下,像这样的间接绑定仍然可以延长生命周期。比如,说:

struct X {
    std::vector<int> v;
};
X foo();

for (auto e : foo().v) {
    // ok!
}
Run Code Online (Sandbox Code Playgroud)

但是,与其试图跟踪所有这些小情况,不如像 Songyuanyao 建议的那样,始终使用带有初始化程序的 new for 语句...

for (auto&& range = f(); auto e : range[5]) {
    // rest of body
}
Run Code Online (Sandbox Code Playgroud)

虽然这在某种程度上给人一种虚假的安全感,因为如果你这样做两次,你仍然会遇到同样的问题......

for (auto&& range = f().g(); auto e : range[5]) {
    // still dangling reference
}
Run Code Online (Sandbox Code Playgroud)


son*_*yao 5

请注意,直接使用临时作为范围表达式是可以的,它的使用时间会延长。但是对于f()[5]f()返回的是临时的,它是在表达式中构造的,它会在构造它的整个表达式之后被销毁。

从 C++20 开始,您可以使用 init-statement for range-based for 循环来解决此类问题。

(强调我的)

如果 range_expression 返回一个临时值,则其生命周期将延长到循环结束,如绑定到右值引用 __range 所指示的那样,但要注意 range_expression 内的任何临时的生命周期都不会延长

可以使用 init-statement 解决此问题:

for (auto& x : foo().items()) { /* .. */ } // undefined behavior if foo() returns by value
for (T thing = foo(); auto& x : thing.items()) { /* ... */ } // OK
Run Code Online (Sandbox Code Playgroud)

例如

for(auto thing = f(); auto e : thing[5])
    std::cout << e << std::endl;
Run Code Online (Sandbox Code Playgroud)