84.范围for

Posted codemagiciant

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了84.范围for相关的知识,希望对你有一定的参考价值。

  C++11新标准引入了一种更简单的for语句,这种语句可以遍历容器或其他序列的所有元素。范围for语句(range for statement)的语法形式是:

for (declaration : expression) 
    statement 

  expression表示的必须是个序列,比如用花括号括起来的初始值列表(参见3.3.1节,第 88页)、数组(参见C++ Primer 3.5节,第101页)或者vector或string等类型的对象,这些类型的共同特点是拥有能返回迭代器的begin和end成员(参见3.4节,第95页)。

  declaration定义个变量,序列中的每个元素都得能转换成该变量的类型(参见4.11 节,第141页)。确保类型相容最简单的办法是使用auto类型说明符(参见2.5.2节,第 61页),这个关键字可以令编译器帮助我们指定合适的类型。如果需要对序列中的元素执行写操作,循环变量必须声明成引用类型。

  每次迭代都会重新定义循环控制变量,并将其初始化成序列中的下一个值,之后才会执行statement。像往常样,statement可以是条单独的语句也可以是一个块。所有元素都处理完毕后循环终止。

  之前我们已经接触过几个这样的循环。接下来的例子将把vector对象中的每个元素都翻倍,它涵盖了范围for语句的几乎所有语法特征:

vector<int>= 0,1,2,3,4,5,6,7,8,9; 
//范围变量必须是引用类型,这样才能对元素执行写操作 
for (auto &r : v)//对于v中的每一个元素
    r *= 2;//将v中每个元素的值翻倍

  for语句头声明了循环控制变量r,并把它和v关联在 起,我们使用关键字auto令编译器为r指定正确的类型。由于准备修改v的元素的值,因此将 r声明成引用类型。此时,在循环体内给r赋值,即改变了r 所绑定的元素的值。

  范围for语句的定义来源千与之等价的传统for语句:

for (auto beg= v.begin(), end= v.end(); beg != end; ++beg) 

    auto &r = *beg;//r必须是引用类型,这样才能对元素执行写操作
    r *= 2;//将v中每个元素的值翻倍

  学习了范围for语句的原理之后,我们也就不难理解为什么在3.3.2节(第90页)强调不能通过范围for语句增加vector对象(或者其他容器)的元素了。在范围for语句中,预存了end()的值。一旦在序列中添加(删除)元素,end函数的值就可能变得无效了(参见3.4. l节,第98页)。关于这 点,将在9.3.6节(第315页)做更详细的介绍。
参考资料:

C++ Primer

临时范围上的基于范围的for循环[重复]

【中文标题】临时范围上的基于范围的for循环[重复]【英文标题】:Range-based for loop on a temporary range [duplicate] 【发布时间】:2018-12-28 09:19:58 【问题描述】:

由于 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 循环中,如第一个示例所示,似乎很容易犯此错误。

【问题讨论】:

这段代码的目的是什么:你想用它来实现什么?因为如果它的唯一目的是锯齿状数组初始化,还有更好的方法。 是的,并且有一个 C++20 提案可以部分解决这个问题:open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0614r1.html @JHBonarius,什么是“锯齿状数组初始化”? 数组数组的初始化... 【参考方案1】:

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

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

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

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

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

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

但是,与其尝试跟踪所有这些小案例,不如像宋元耀建议的那样,始终使用带有初始化程序的新 for 语句……

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&amp; __range = f()[5]; // (*),语法糖会“更好”(对于这个问题)吗? @alfC 不。同样的结果,只是你得到一个 const 左值引用而不是非 const 右值引用。你肯定想要auto&amp;&amp; 那里,否则你只会得到const 访问权限。 好的,我在想... const&amp; 会延长寿命。至于 const 访问,原则上取决于.begin() 的语义。实际上你是对的。 我质疑“[使用新的初始化程序功能]要好得多”。非常整洁的循环(我认为,这就是基于范围的 for 循环的全部要点)突然变得相当混乱,在我看来,我们已经失去了很多以前获得的基于范围的东西.至于“基于范围的只是...的语法糖”;理解了,我希望它在未来会变成语法糖,用于。【参考方案2】:

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

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

(强调我的)

如果 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

例如

for(auto thing = f(); auto e : thing[5])
    std::cout << e << std::endl;

【讨论】:

酷。初始语句也可以用于正常的 for 循环吗? 好吧,但不要预定义任意类型的辅助变量。 @alfC 我明白你的意思;正常的for循环似乎是不可能的。 例如,它可以接受(通过修改语言)类似于 lambda 捕获的内容,例如for[thing = foo()](auto it = thing.begin();; it != thing.end(); ++it). @alfC 是的;你可能想为此提出建议。 :)

以上是关于84.范围for的主要内容,如果未能解决你的问题,请参考以下文章