【问题标题】:Why are Standard iterator ranges [begin, end) instead of [begin, end]?为什么标准迭代器范围是 [begin, end) 而不是 [begin, end]?
【发布时间】:2020-10-14 16:46:04
【问题描述】:

为什么标准将end() 定义为末尾,而不是实际末尾?

【问题讨论】:

  • 我猜“因为这就是标准所说的”不会削减它,对吧? :)
  • @LuchianGrigore:当然不是。这会削弱我们对标准(背后的人)的尊重。我们应该期望标准做出的选择有一个原因
  • 我猜,这个解释也值得你关注:One Past the End
  • 简而言之,计算机算不上人。但是,如果您对为什么人们不把计算机算作计算机感到好奇,我建议您使用The Nothing that Is: A Natural History of Zero 来深入了解人类发现有一个小于一的数字时遇到的麻烦。
  • 因为只有一种方法可以生成“最后一个”,所以它通常并不便宜,因为它必须是真实的。生成“你从悬崖的尽头掉下来”总是很便宜,许多可能的表示都可以。 (void*)"ahhhhhhh" 会很好。

标签: c++ stl iterator standards


【解决方案1】:

最好的论据很容易是Dijkstra himself提出的论据:

  • 你希望范围的大小是一个简单的差异end - begin;

  • 当序列退化为空序列时,包含下限更“自然”,而且因为替代方案(排除下限)需要存在“one-before” -the-beginning" 标记值。

你仍然需要证明你为什么从零开始计数而不是一,但这不是你问题的一部分。

当您拥有任何类型的算法来处理对基于范围的构造的多个嵌套或迭代调用时,[begin, end) 约定背后的智慧一次又一次地得到回报,这些构造自然地链接在一起。相比之下,使用双重封闭的范围会导致错误和非常不愉快和嘈杂的代码。例如,考虑一个分区 [n0, n1)[n 1, n2)[n2,n em>3)。另一个例子是标准迭代循环for (it = begin; it != end; ++it),它运行end - begin 次。如果两端都包含在内,则相应的代码的可读性会大大降低——想象一下您将如何处理空范围。

最后,我们还可以提出一个很好的论据,为什么计数应该从零开始:根据我们刚刚建立的范围的半开约定,如果给定一个 N 个元素的范围(比如枚举数组的成员),然后 0 是自然的“开始”,因此您可以将范围写为 [0, N),没有任何尴尬的偏移或更正。

简而言之:我们在基于范围的算法中没有看到数字1 的事实是[begin, end) 约定的直接结果和动机。

【讨论】:

  • 在大小为 N 的数组上迭代的典型 C for 循环是“for(i=0;i
  • @KrazyGlew:我没有故意将类型放入循环示例中。如果您将beginend 视为ints,其值分别为0N,那么它非常适合。可以说,!= 条件比传统的 < 更自然,但直到我们开始考虑更一般的集合时,我们才发现这一点。
  • @KerrekSB:我同意“在我们开始考虑更通用的集合之前,我们从未发现 [!= 更好]。”恕我直言,这是 Stepanov 值得称赞的事情之一 - 作为一个试图在 STL 之前编写此类模板库的人发言。但是,我会争论“!=”更自然 - 或者,更确切地说,我会争论 != 可能引入了错误,而
  • @KrazyGlew:您的最后一点有点离题,因为序列 {0, 3, 6, ..., 99} 不是 OP 询问的形式。如果您希望它是这样,您应该编写一个++-incrementable 迭代器模板step_by<3>,然后它就会具有最初宣传的语义。
  • @KrazyGlew 即使 无论如何它都是错误。如果有人在他应该使用< 的时候使用!=,那么这是一个 错误。顺便说一句,通过单元测试或断言很容易找到错误之王。
【解决方案2】:

实际上,如果考虑到迭代器不是指向 序列的元素而是 位于它们之间,那么很多与迭代器相关的东西突然变得更有意义了,通过解引用访问下一个元素。然后“过去的结束”迭代器突然变得有意义:

   +---+---+---+---+
   | A | B | C | D |
   +---+---+---+---+
   ^               ^
   |               |
 begin            end

显然begin指向序列的开头,end指向同一个序列的结尾。取消引用begin 会访问元素A,而取消引用end 没有任何意义,因为它没有正确的元素。此外,在中间添加一个迭代器 i 给出了

   +---+---+---+---+
   | A | B | C | D |
   +---+---+---+---+
   ^       ^       ^
   |       |       |
 begin     i      end

您会立即看到从begini 的元素范围包含元素AB,而从iend 的元素范围包含元素CD。取消引用i 会给出它的右侧元素,即第二个序列的第一个元素。

即使是反向迭代器的“一对一”也突然变得很明显:反转该序列给出:

   +---+---+---+---+
   | D | C | B | A |
   +---+---+---+---+
   ^       ^       ^
   |       |       |
rbegin     ri     rend
 (end)    (i)   (begin)

我已经在下面的括号中编写了相应的非反向(基本)迭代器。你看,属于i(我命名为ri)的反向迭代器仍然指向元素BC之间。但是由于颠倒了顺序,现在元素 B 在它的右边。

【讨论】:

  • 这是恕我直言的最佳答案,但我认为如果迭代器指向数字并且元素在数字之间(语法foo[i])是该项目的简写立即位置i)。想一想,我想知道对于一种语言来说,为“位置 i 之后的项目”和“位置 i 之前的项目”设置单独的运算符是否有用,因为许多算法都处理成对的相邻项目,并说“位置 i 两侧的项目可能比“位置 i 和 i+1 的项目”更干净。
  • @supercat:这些数字不应该表示迭代器位置/索引,而是表示元素本身。我将用字母替换数字以使其更清晰。实际上,根据给定的数字,begin[0](假设是随机访问迭代器)将访问元素 1,因为在我的示例序列中没有元素 0
  • 为什么用“开始”这个词而不是“开始”?毕竟,“开始”是一个动词。
  • @user1741137 我认为“开始”是“开始”的缩写(现在有意义)。 “开始”太长,“开始”听起来很合适。 “start”会与动词“start”发生冲突(例如,当您必须在类中定义函数start() 以启动特定进程或其他任何内容时,如果它与已经存在的进程冲突会很烦人)。
【解决方案3】:

为什么标准将end() 定义为末尾,而不是实际末尾?

因为:

  1. 它避免了对空范围的特殊处理。对于空范围,begin() 等于 end() &
  2. 它使迭代元素的循环的结束标准变得简单:循环简单 只要没有达到end(),就继续。

【讨论】:

    【解决方案4】:

    因为那时

    size() == end() - begin()   // For iterators for whom subtraction is valid
    

    而且您不必做尴尬之类的事情

    // Never mind that this is INVALID for input iterators...
    bool empty() { return begin() == end() + 1; }
    

    而且你不会不小心写出错误代码之类的

    bool empty() { return begin() == end() - 1; }    // a typo from the first version
                                                     // of this post
                                                     // (see, it really is confusing)
    
    bool empty() { return end() - begin() == -1; }   // Signed/unsigned mismatch
    // Plus the fact that subtracting is also invalid for many iterators
    

    另外:如果end() 指向一个有效元素,find() 会返回什么?
    真的想要另一个名为invalid()的成员返回一个无效的迭代器吗?!
    两个迭代器已经够痛苦了……

    哦,还有查看this相关帖子


    还有:

    如果end在最后一个元素之前,你会如何insert()在真正的结尾?!

    【讨论】:

    • 这是一个被严重低估的答案。这些例子简明扼要,而且“也”不是别人说的,回想起来似乎很明显,但像启示一样打动了我。
    • @underscore_d:谢谢!! :)
    • 顺便说一句,如果我看起来像一个不赞成的伪君子,那是因为我早在 2016 年 7 月就已经这样做了!
    • @underscore_d:哈哈哈我都没注意到,但是谢谢! :)
    【解决方案5】:

    半封闭范围[begin(), end()) 的迭代器习惯用法最初是基于普通数组的指针算法。在这种操作模式下,您将拥有传递数组和大小的函数。

    void func(int* array, size_t size)
    

    当您拥有这些信息时,转换为半封闭范围[begin, end) 非常简单:

    int* begin;
    int* end = array + size;
    
    for (int* it = begin; it < end; ++it) { ... }
    

    要使用完全封闭的范围,它更难:

    int* begin;
    int* end = array + size - 1;
    
    for (int* it = begin; it <= end; ++it) { ... }
    

    由于指向数组的指针在 C++ 中是迭代器(并且语法旨在允许这样做),因此调用 std::find(array, array + size, some_value) 比调用 std::find(array, array + size - 1, some_value) 容易得多。


    另外,如果您使用半封闭范围,您可以使用 != 运算符来检查结束条件,因为(如果您的运算符定义正确)&lt; 意味着 !=

    for (int* it = begin; it != end; ++ it) { ... }
    

    但是,对于完全封闭的范围,没有简单的方法可以做到这一点。你被&lt;=困住了。

    在 C++ 中唯一支持 &lt;&gt; 操作的迭代器是随机访问迭代器。如果必须为 C++ 中的每个迭代器类编写 &lt;= 运算符,则必须使所有迭代器完全可比,并且创建功能较差的迭代器的选择更少(例如 @987654336 上的双向迭代器@,或在 iostreams) 上运行的输入迭代器(如果 C++ 使用全封闭范围)。

    【讨论】:

      【解决方案6】:

      end() 指向末尾,很容易用 for 循环迭代集合:

      for (iterator it = collection.begin(); it != collection.end(); it++)
      {
          DoStuff(*it);
      }
      

      end() 指向最后一个元素,循环会更复杂:

      iterator it = collection.begin();
      while (!collection.empty())
      {
          DoStuff(*it);
      
          if (it == collection.end())
              break;
      
          it++;
      }
      

      【讨论】:

        【解决方案7】:
        1. 如果容器是空的,begin() == end()
        2. C++ 程序员倾向于在循环条件中使用!= 而不是&lt;(小于),因此 让end() 指向一个末端位置很方便。

        【讨论】:

          猜你喜欢
          • 2011-10-16
          • 1970-01-01
          • 2014-01-24
          • 2016-11-26
          • 2016-04-10
          • 2013-12-06
          相关资源
          最近更新 更多