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的主要内容,如果未能解决你的问题,请参考以下文章

Educational Codeforces Round 84 (Rated for Div. 2)

Educational Codeforces Round 84 (Rated for Div. 2)

Educational Codeforces Round 84 (Rated for Div. 2)E(组合数学)

JAVA代码根据经纬度范围计算WGS84与谷歌全球墨卡托包含的切片数目与拼接图像像素尺寸

Educational Codeforces Round 84 (Rated for Div. 2)

Educational Codeforces Round 84 (Rated for Div. 2) C. Game with Chips(思维题)