STL容器中基于范围的循环中迭代器语法之间的区别是什么

时间:2016-07-13 10:48:13

标签: c++ c++11 stl iterator

虽然我对Java中的C和OOP了解不多,但我开始深入研究C ++及其特性。我已经阅读了有关C ++的所有基本知识,但我仍然对某些特定于C ++ 11的东西感到困惑,包括语法和性能方面的问题。在这些事情上是容器迭代器,我发现它以无数的语法形式实现(例如range-based loops)。

我想知道哪些是完全等效的,为什么会想要使用其中一个,以及对性能有何影响。

a)auto vs明确声明:

是否始终支持auto?除了代码可读性问题,为什么程序员更喜欢显式声明?

list<int>::const_iterator i = myIntList.begin();    /* Option a1 */
auto i = myIntList.begin();                         /* Option a2 */

for(auto i : myIntList) { ... }                     /* Option a3 */
for(int i : myIntList) { ... }                      /* Option a4 */

b)紧凑形式与扩展循环形式

list<int> l = {1, 2, 3, ...};
for(auto i : l) { ... }                             /* Option b1 */
for(auto i = l.begin(); i != l.end(); ++i) { ... }  /* Option b2 */
c)常量,非常量/访问类型

为什么/何时更喜欢在循环体中使用引用或常量?

/* Constant/non-constant: */
for(list<int>iterator i = l.begin(); ...) { ... }   /* Option c1 */
for(list<int>const_iterator i = l.begin(); ...) { } /* Option c2 */

for(const int& i : list) { ... }                    /* Option c3 */
for(int& i : list) { ... }                          /* Option c4 */

/* Access by reference/by value: */
for(auto&& i : list) { ... }                        /* Option c5 */
for(auto i : list) { ... }                          /* Option c6 */

d)循环退出条件:

/* Option d1: end is defined within the start condition or outside the loop. */
for(auto i = l.begin(), end = l.end(); i != end; ++i) { ... }

/* Option d2: end is defined in the continue condition. */
for(auto i = l.begin(); i != l.end(); ++i) { ... }

也许大多数都是相同的,也许一个选项或另一个选项的选择只对给定的循环体有意义,但我想知道允许这么多可能的方法编程相同行为的目的是什么。

5 个答案:

答案 0 :(得分:4)

  

是否始终支持自动?

仅从C ++ 11开始,但基于范围的循环也是如此,所以如果你可以依赖一个,那么你应该被允许依赖另一个。

  

除了代码可读性问题之外,为什么程序员更喜欢显式声明?

您可以使用显式类型隐式转换为其他类型。

  

紧凑形式与扩展循环形式

你可以通过显式使用迭代器(你称之为“扩展”)来做更多的事情,而不是使用基于范围的循环(你称之为“紧凑”)。但是,如果您只想迭代元素范围一次,那么基于范围的循环具有更简单的语法。这就是它被引入语言的原因。

  

为什么/何时更喜欢在循环体中使用引用[...]?

当一个人无法复制或想要避免复制迭代元素时。

  

为什么/何时更喜欢在循环体中有一个常数?

当一个人只有const访问范围,或者想表达他们不打算修改对象时。

  

循环的退出条件

如果循环中的结束指针无效,则只有d2是正确的。

如果结束迭代器是不变的,那么d1由于将函数调用带出循环而稍微提高效率。

如果编译器可以看到T::end()的定义,那么可以通过将d2转换为d1进行优化。

在任何情况下,单个函数调用的开销通常可以忽略不计,除非循环体本身很简单。

  

我想知道允许编程相同行为的许多可能方法的目的是什么。

确实可以使用goto实现所有循环结构。

那么,为什么c ++会允许任何其他循环结构呢?引入了forwhile等,使程序更易于理解,更具可读性。好的,那么当goto更容易理解时,为什么不摆脱for?这是因为for无法完成goto所能做的所有事情。 goto更为通用。

相同的推理适用于这种新的基于范围的循环。它比更通用的循环结构更容易推理,因此是有用的补充。但是一般结构仍然可以用于基于范围的循环不可能的用途。此外,删除一般for结构会破坏语言的向后兼容性,这是不可取的。

答案 1 :(得分:3)

所有形式都是替代品,与其他形式相比,每种形式都有优点和缺点。

1)当你想要一个不同于auto的类型可能推断时,首选显式声明循环变量。如果有必要在C ++ 11之前保持与C ++实现的兼容性,那也是强制性的(是的,有必要的实际实际情况 - 改变编译器需要付出代价,就像维护成本一样旧的)。

2)&#34; compact&#34;如果需求不是在一个范围内的所有元素上顺序迭代,那么form(更准确地说,基于范围的循环)是不合适的。例如,如果循环遍历每个第二个元素,如果循环体由于某种原因调整容器大小(使迭代器无效)。

3)const限定符表示意图是循环不会更改容器的元素。这对于让编译器诊断循环(可能)导致元素意外更改的问题非常有用。例如呼叫非const成员。在不使用const限定符的情况下,有很多情况下这些问题是很难找到的错误。

4)如果循环体以任何方式调整容器大小(因为它使迭代器无效),则在开始条件内定义结束条件会导致未定义的行为。在每次循环迭代中重新计算结束条件可以防止此类问题。

具有如此多的不同编写循环方式的目的是程序员的便利性。根据程序员的尝试,不同的技术可能是合适的。

权衡是有时很难决定最合适的&#34;循环形式。

答案 2 :(得分:2)

基于范围的for(:)循环在C ++中非常常见,根据for(;;)循环定义。在C ++ 17之前,它与选项 d1 基本相同,注意到迭代器变量在客户端代码中是不可见的。

请注意,基于范围的迭代遍历元素,而不是有效的迭代器。你可以根据迭代器制作一个范围,但它需要一些胶水代码。

修改了基于C ++ 17范围的for(:),以便end允许begin具有与auto不同的类型。这对于s​​entinal技术很有用,但在这一点上并没有那么重要。

const总是像它一样工作。除了表达模板的幻想,它总是以人们可以轻易理解的方式工作。使用它可以使类型更难以解决,但有时你不在乎,有时你只是不必要地重复这种类型。如果搞砸了,不使用它会导致令人惊讶的类型不匹配。

引用是别名。值是副本。如果ypu想要遍历副本,你可以。如果要将别名迭代到容器的内容,则可以。

同样,您可以拥有容器内容的const或非const视图。你可以选择,这取决于你需要什么或想要什么。

偏向const和值可以使代码更轻松地为程序员和编译器解码。然而,复杂结构的不必要副本可能很昂贵,end可以抑制移动和隐式移动以及其他突变效率技巧。

如果你缓存for(:)(d1 vs d2),它有时会稍快一些。但通常不值得注意,而且是噪音。理论上,如果容器发生变化,非缓存端可以更好地工作,但是在迭代时更改容器通常是疯狂的,并且需要修改advance子句以及终止。 auto&&循环缓存结束,因为噪声参数消失了。

begin推导出变量的转发引用:它可以是左值引用,也可以是右值引用到临时。这意味着&#34;我不在乎,不要复制任何东西,但让我使用它&#34;。参考生命周期扩展通常会使悬空引用成为问题,只要您绑定的数据源不会搞砸。

您缺少的另一件大事是非会员for(:)。这允许访问C风格的数组,就好像它们在范围内一样,并且如果以正确的方式完成,则可以轻松地向第三方范围添加范围支持,因此 class UserResource(ModelResource): class Meta: queryset = CustomUser.objects.all() resource_name = 'user' allowed_methods = ['get','post'] filtering = {"id": ALL} excludes = ['is_staff','password','is_superuser','id','is_active','date_joined'] authentication = BasicAuthentication() 也适用于它们。

答案 3 :(得分:1)

  

[a]是否始终支持auto?除了代码可读性问题,为什么程序员更喜欢显式声明?

几乎总是肯定的,如果他们想要在LHS上执行从RHS类型到非相同类型的转换,他们会更喜欢显式输入......或者只是不喜欢隐式的东西。

  

b)紧凑形式与扩展循环形式

这里有问题吗?当然,不同之处在于您不必取消引用范围内的迭代器 - for

如果您的算法要求您使用迭代器和/或使其无效的操作(例如插入,擦除),那么您需要使用迭代器,因此请使用&#39; old&#39; /&#39;手册&# 39;语法。

但如果你不这样做,那么范围 - for可以避免因为你做这件事而不得不取消引用迭代器的麻烦。

  

[c]为什么/何时更喜欢在循环体中有引用或常量?

引用或常量之间的选择,就像其他地方一样 - 特别是包括几乎相同的函数参数选择 - 取决于

  • 是否要更改元素,因此&
  • 即使没有,在每次迭代时按值复制到循环变量是否会很昂贵,因此const &
  

d)循环退出条件:

第二个变体缓存end()迭代器,即

  • 可能更快,但检查编译输出以验证
  • 如果循环中的任何操作使end()迭代器
  • 无效,则无效

答案 4 :(得分:1)

a)auto vs明确声明:

始终支持

auto,因为所有类型在编译时都是已知的。

显式声明类型的最重要原因是代码可读性,因为它可以在以后读取代码时更容易找出类型。此外,如果存在隐式转换,则明确声明类型允许您将项目转换为其他类型。

b)紧凑形式与扩展循环形式:

两者完全相同。你应该尽可能使用紧凑的形式。

c)常数,非常数/访问类型:

您希望使用const引用而不是可变引用的原因是 const correctness ,这是一种保护机制,可以防止您在不应该使用时改变对象。特别是,如果您只对容器本身有const引用,则只能在循环体中使用const引用或值的副本。

一个常见的建议是尽可能使用const,至少使用方法参数。如果方法接收到对象的const引用,则可以放心该方法不会改变对象(除非某处有const_cast)。

按值访问项目是最短​​的选项,即使使用const容器也可以使用,如果项类型是小标量类型(bool,{{1},则不会有任何性能损失}},int等。这是你默认想要做的事情。

如果项目属于不可复制类型,或者您想要改变它们,则需要通过引用访问这些项目。此外,如果它们属于大类类型,则通过引用访问项目会更快。

d)循环退出条件:

选项double只调用d2一次,理论上稍快一些。但是,实际上l.end()是一种非常快速的方法。选项end()几乎总是首选,因为它更短。

允许多种方式的目的

例如,考虑循环。 C ++是从C演变而来的,它支持C类循环,目的是熟悉C程序员并向后兼容现有的C代码。

基于范围的for循环只是旧循环方法的语法糖。没有理由不允许使用旧的循环语法:此外,禁用它会很成问题,因为在C ++ 11之前的循环中使用的语言结构在其他地方仍然有用。旧的d1语法对循环数字之类的内容非常有用:

for

同样,for (int i = 0; i < 5; ++i) std::cout << i; begin()方法返回迭代器,例如,传递给end()中的标准算法。

真的,C ++委员会没有任何合理的方法来防止旧式循环,也没有删除语言中的有用功能或制作非常奇怪的特殊情况(例如禁止<algorithm>begin()end()语句中调用。他们也没有任何理由这样做。

其他情况类似:有多种方法可以做同样的事情,因为这只是语言特征的交互方式,没有理由试图阻止这种情况。