【问题标题】:What's the difference between iterator syntax in range-based loops for STL containers [closed]STL容器的基于范围的循环中的迭代器语法有什么区别[关闭]
【发布时间】:2016-07-13 10:48:12
【问题描述】:

虽然我对 Java 中的 C 和 OOP 非常了解,但我开始深入研究 C++ 及其特殊性。我已经阅读了有关 C++ 的所有基本内容,但我仍然对一些 C++11 特定的东西感到困惑,无论是语法还是性能方面。其中包括容器迭代器,我发现它以多种语法形式实现(例如range-based loops)。

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

a) auto 与显式声明:

是否始终支持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) { ... }

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

【问题讨论】:

  • 您指的是标准容器吗?
  • 也迭代元素与迭代器。我想你在这里问的太多了。
  • I wonder what's the purpose of allowing so many possible ways of programming the same behaviour. 因为该语言必须保持与旧版本的兼容性,同时添加改进的语法。就这么简单。您问的其他所有问题都已在其他地方讨论到死。标记为过于宽泛。
  • @underscore_d 实际上,大多数“可能的方式”实际上做了不同的事情。
  • @juanchopanza 是的,我的意思是 range-for 通常是可以使用显式基于迭代器的语法指定的各种事物的简写,但可能过于简单化了。我仍然认为这个“问题”实际上是很多问题,所有这些问题都在其他地方得到了充分的回答,无论是在 SO 上还是在一个好的 C++ 参考中。

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


【解决方案1】:

是否始终支持自动?

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


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

您可以使用显式类型将隐式转换为另一种类型。


紧凑形式与扩展循环形式

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


为什么/什么时候希望在循环体中有一个引用 [...]?

当一个人不能复制,或者想要避免复制被迭代的元素时。

为什么/何时更愿意在循环体中使用 [...] 常量?

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


循环的退出条件

如果结束指针在循环内失效,那么只有d2是正确的。

如果结束迭代器是不变的,那么d1 可能会因为将函数调用带出循环而稍微更有效。

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

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


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

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

那么,为什么 c++ 会允许任何其他循环结构?引入了forwhile 等以使程序更易于理解、更具可读性。好的,那为什么不去掉goto,当for 更容易理解呢?那是因为for 不能做所有goto 可以做的事情。 goto 更笼统。

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

【讨论】:

    【解决方案2】:

    所有的形式都是可以选择的,各有优劣。

    1) 当你想要一个不同于auto 的类型时,最好显式声明循环变量。如果有必要保持与 C++11 之前的 C++ 实现的兼容性,这也是强制性的(是的,在实际的现实世界中,这是必要的 - 更改编译器是有成本的,就像维护是有成本的一样旧的)。

    2) “紧凑”形式(更准确地说,是基于范围的循环)如果要求不是按顺序遍历范围内的所有元素,则不适合。例如,如果循环遍历每个第二个元素,如果循环体出于某种原因调整了容器的大小(这会使迭代器无效)。

    3) const 限定符表示一个意图是循环不会更改容器的元素。这对于让编译器诊断循环(可能)对元素进行意外更改的问题非常有用。例如调用非const 成员。如果不使用 const 限定符,在许多情况下此类问题是很难追踪的错误。

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

    有这么多不同的循环编写方式的目的是为了方便程序员。根据程序员的尝试,不同的技术可能是合适的。

    权衡是有时很难确定最“合适”的循环形式。

    【讨论】:

      【解决方案3】:

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

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

      基于 for(:) 的后 C++17 范围已修改,因此它允许 end 具有与 begin 不同的类型。这对哨兵技术很有用,但在这一点上并没有那么重要的区别。

      auto 总是这样工作。除非表达式模板花哨,它总是以易于理解的方式工作。使用它会使类型变得更难处理,但有时你不在乎,有时你只是不必要地重复类型。如果你搞砸了,不使用它可能会导致令人惊讶的类型不匹配。

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

      同样,您可以拥有容器内容的const 或非const 视图。您可以根据需要或想要的内容进行选择。

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

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

      auto&amp;&amp; 推导出变量的转发引用:它可以是左值引用,也可以是右值临时引用。这意味着“我不在乎,不要复制任何东西,但让我使用它”。引用生命周期延长通常会使悬空引用不成问题,只要您绑定的数据源没有搞砸。

      您缺少的另一件大事是非会员begin。这允许访问 C 样式的数组,就好像它们在范围内一样,如果以正确的方式完成,您可以轻松地将范围支持添加到第三方范围之类,因此 for(:) 也适用于它们。

      【讨论】:

        【解决方案4】:

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

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

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

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

        如果您的算法要求您使用迭代器和/或使它们无效的操作(例如插入、擦除),那么您需要迭代器,因此请使用“旧”/“手动”语法。

        但是,如果您不这样做,那么 range-for 可以通过为您执行该操作来避免必须取消引用迭代器的麻烦。

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

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

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

        d) 循环的退出条件:

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

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

        【讨论】:

          【解决方案5】:

          a) auto 与显式声明:

          auto 始终受到支持,因为所有类型在编译时都是已知的。

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

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

          两者完全等价。您应该尽可能使用紧凑形式。

          c) 常量、非常量/访问类型:

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

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

          按值访问项目是最短​​的选项,即使使用 const 容器也可以,并且如果项目类型是小标量类型(boolintdouble等等)这是你默认想要做的。

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

          d) 循环的退出条件:

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

          允许多种方式的目的

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

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

          for (int i = 0; i < 5; ++i)
              std::cout << i;
          

          同样,begin()end() 方法返回迭代器,例如在&lt;algorithm&gt; 中传递给标准算法。

          真的,C++ 委员会没有任何合理的方法来防止旧式循环,同时又不删除语言中的有用功能或制造非常奇怪的特殊情况(例如不允许在for 声明)。他们也没有任何理由这样做。

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

          【讨论】:

          • 不错的答案,但我要补充一点,如果迭代器需要进行操作或者可以通过循环体中的操作无效,则 range-for 几乎肯定不会削减它。正如您简要指出的那样,这可能是关键原因,即“旧”循环方法对于不太简单的循环仍然具有非常有效的用例。
          猜你喜欢
          • 2015-04-30
          • 2014-12-05
          • 2020-10-14
          • 2013-01-27
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2021-12-11
          • 1970-01-01
          相关资源
          最近更新 更多