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

16

由于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;
}
似乎beginend来自一个临时的变量并在循环中丢失。
当然,一个解决方法是进行以下操作。
    auto r = f()[5];
    for(auto e : r)
        std::cout << e << std::endl;

然而,我想知道为什么for(auto e : f()[5])是错误的,并且是否有更好的方法来避免这种问题,或者设计f或容器(std::vector)以避免这种陷阱。

使用迭代器循环更容易理解为什么会出现这个问题(beginend来自不同的临时对象)

for(auto it = f()[5].begin(); it != f()[5].end(); ++it)

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


这段代码的目的是什么:你想用它实现什么?因为如果它唯一的目的是不规则数组的初始化,那么有更好的方法。 - JHBonarius
2
是的,而且有一个C++20的提案部分解决了这个问题:http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0614r1.html - bipll
@JHBonarius,什么是“锯齿数组初始化”? - alfC
数组的数组初始化... - JHBonarius
2个回答

14

我想知道为什么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
    }
}

看一下第一行,会发生什么?vectoroperator[]返回对象的引用,因此__range被绑定到该内部引用。但是然后临时变量在行末离开作用域,销毁了所有内部内容,__range立即成为悬空引用。这里没有寿命延长,我们从未将引用绑定到临时变量。

在更正常的情况下,for(auto e : f()),我们将直接将__range绑定到f(),这是将引用绑定到临时变量,因此该临时变量的寿命将得到延长,直到引用的寿命结束,也就是完整的for语句的生命周期。

要增加更多的复杂性,有其他情况下,像这样的间接绑定仍然会进行生命期扩展。例如:

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

for (auto e : foo().v) {
    // ok!
}

不过,与其试图跟踪所有这些小案例,使用带有初始化器的新的for语句会更好,就像songyuanyao建议的那样...一直使用:

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

虽然从某种程度上来说,这会给人一种错误的安全感,因为如果你重复做了一遍,仍然会有同样的问题...

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

如果使用auto const& __range = f()[5]; // (*)这种语法糖,对于这个问题来说会更好吗? - alfC
@alfC 不是的,结果是一样的,只是你会得到一个const左值引用而不是非const右值引用。你绝对需要在那里使用auto&&,否则你只能获得const访问权限。 - Barry
好的,我曾经认为 ... const& 会延长生命周期。至于常量访问,这取决于 .begin() 的语义原理。实际上你是对的。 - alfC
我对“使用新的初始化器功能更好”的观点提出质疑。我认为,非常简洁的循环(这应该是基于范围的for循环的全部意义)突然变得相当混乱,而且在我看来,我们失去了许多之前基于范围的for所获得的优势。至于“基于范围的for只是语法糖...”;我理解,我希望它将来会被改成为语法糖,以便使用具有更少令人惊讶的生命周期语义的<不同代码片段>。 - Don Hatch

9
注意直接使用临时变量作为区间表达式是可以的,它的寿命会被延长。但对于 f()[5]f() 返回的是临时变量,并在表达式中构造,它将在其构造所在的整个表达式之后被销毁。
从 C++20 开始,您可以使用 init-statement 为 range-based for loop 解决此类问题。
(强调是我的)

If range_expression returns a temporary, its lifetime is extended until the end of the loop, as indicated by binding to the rvalue reference __range, but beware that the lifetime of any temporary within range_expression is not extended.

This problem may be worked around using init-statement:

for (auto& x : foo().items()) { /* .. */ } // undefined behavior if foo() returns by value
for (T thing = foo(); auto& x : thing.items()) { /* ... */ } // OK
例如。
for(auto thing = f(); auto e : thing[5])
    std::cout << e << std::endl;

好的。初始语句也可以在普通的for循环中使用吗? - alfC
好的,但不要预定义任意类型的辅助变量。 - alfC
@alfC 是的;而且您可能想为此提出建议。 :) - songyuanyao
为什么 for (auto& x : foo().items()) { /* .. */ } 是未定义行为。如果 items() 函数返回一个值,比如一个返回 std::vector<int> items(){....} 的函数,那么这个向量不是会根据生命周期扩展规则来延长其生命周期吗?即使 foo() 可能是临时的,在这种情况下只要 items() 是按值返回的,似乎也没问题。除非我漏了些什么。 - cogle
@cogle 你说得对。这里的重点是items()返回的是引用;由foo()返回的临时对象所返回的引用在临时对象被销毁后就会变成悬垂引用。就像OP问题中的情况一样,operator[]也是返回引用。如果像你所说的那样,items()返回值而不是引用,那么就没问题了。 - songyuanyao
显示剩余4条评论

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