为什么标准迭代器范围是[begin,end]而不是[begin,end]?


204

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


19
我猜“因为这就是标准所说的内容”不会削减,对吧?:)
Luchian Grigore'4

39
@LuchianGrigore:当然不是。那会削弱我们对标准背后的人的尊重。我们应该期望标准有选择的理由
Kerrek SB

4
简而言之,计算机并不像人一样重要。但是,如果您对人们为什么不像计算机那样感到好奇,我建议您阅读《无所不为:零自然历史》,深入了解人类发现一个数字比一个数字少的麻烦。比一个。
约翰·麦克法兰

8
因为只有一种生成“最后一个”的方法,所以它通常并不便宜,因为它必须是真实的。生成“您从悬崖的尽头摔下来” 总是很便宜的,许多可能的表示都会这样做。(void *)“ ahhhhhhh”会很好。
汉斯·帕桑

6
我看了问问题的日期,有一秒钟我以为你在开玩笑。
Asaf 2012年

Answers:


286

最好的论据是Dijkstra本人提出的

  • 你想要的范围的大小是一个简单的区别结束  -  开始 ;

  • 当序列退化为空序列时,包含下界更“自然”,并且还因为替代方案(不包括下界)将需要存在“一个从头开始”的前哨值。

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

[begin,end)约定背后的智慧一次又一次地获得回报,当您拥有某种类型的算法来处理对基于范围的构造的多个嵌套或迭代调用时,这种调用自然会链接。相比之下,使用双封闭范围会产生异样的结果以及极其不愉快和嘈杂的代码。例如,考虑分区[ n 0n 1 ] [ n 1n 2)[ n 2n 3)。另一个例子是标准迭代循环for (it = begin; it != end; ++it),它运行end - begin时间。如果两端都包含在内,则相应的代码将不那么易读-想象一下如何处理空范围。

最后,我们还可以做出一个很好的论证,为什么计数应从零开始:对于刚刚建立的范围,采用半开式约定,如果给定N个元素的范围(例如,枚举数组的成员),则0是自然的“开始”,因此您可以将范围写为[0,N),而无需任何笨拙的偏移或校正。

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


2
在大小为N的数组上循环的典型C for循环是“ for(i = 0; i <N; i ++)a [i] = 0;”。现在,您无法直接使用迭代器来表达这一点-许多人都在浪费时间尝试使<有意义。但是说“ for(i = 0; i!= N; i ++)...”几乎同样明显,因此将0映射到开始,将N映射到结束很方便。
Krazy Glew 2012年

3
@KrazyGlew:我没有故意将类型放入循环示例中。如果您将begin和分别end视为int带有0和的N,则非常适合。可以说,这是!=比传统更为自然的条件<,但是直到我们开始考虑更通用的系列之前,我们才发现这一点。
Kerrek SB 2012年

4
@KerrekSB:我同意“直到我们开始考虑更多常规收藏之前,我们才发现[!=更好]”。恕我直言,这是Stepanov值得称赞的事情之一-作为试图在STL之前编写此类模板库的人发言。但是,我将争论“!=”更自然-或更确切地说,我将争论!=可能引入了bug,而<将会引起注意。想想(i = 0; i!= 100; i + = 3)...
Krazy Glew 2012年

@KrazyGlew:您的最后一点有点离题,因为序列{0,3,6,...,99}的格式不是OP所要求的。如果您希望这样做,则应编写一个++-incrementable迭代器模板step_by<3>,该模板将具有最初宣传的语义。
Kerrek SB

@KrazyGlew即使<有时会隐藏一个错误,无论如何还是一个错误。如果有人使用!=时应该使用<那是一个错误。顺便说一句,通过单元测试或断言很容易发现错误之王。
Phil1970年

80

其实,很多的迭代器相关的东西突然更有道理,如果你考虑不迭代器指向该序列的元素,但在两者之间,与非关联访问的下一个元素的权利吧。然后,“ one end end”迭代器突然变得有意义:

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

显然begin指向序列的开头,并end指向同一序列的结尾。取消引用begin访问element 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)的反向迭代器指向元素B和之间C。但是由于反转顺序,现在元素B在它的右边。


2
这是恕我直言的最佳答案,尽管我认为,如果迭代器指向数字,并且元素位于数字之间,则可能会更好地说明(语法 foo[i])是位置()之后的缩写,i。考虑一下,我想知道一种语言对于“位置i之后的项目”和“位置i之后的项目”使用单独的运算符是否有用,因为许多算法都处理成对的相邻项目,并说“ “位置i两侧的项目”可能比“位置i和i + 1的项目”干净。
2015年

@supercat:这些数字不应指示迭代器的位置/索引,而应指示元素本身。我将数字替换为字母以使其更清楚。确实,根据给出的数字,begin[0](假设有一个随机访问迭代器)将访问element 1,因为0在我的示例序列中没有元素。
celtschk 2015年

为什么使用“开始”一词而不是“开始”?毕竟,“开始”是一个动词。
user1741137

@ user1741137我认为“开始”是“开始”的缩写(现在有意义)。“开始”太长,“开始”听起来很合适。“开始”将与动词“开始”冲突(例如,当您必须start()在类中定义一个函数来启动特定过程或其他任何功能时,如果它与已经存在的过程发生冲突将很烦人)。
Fareanor

74

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

因为:

  1. 避免对空范围进行特殊处理。对于空范围,begin()等于 end()
  2. 对于循环遍历元素的循环,它使结束条件变得简单:只要end()没有达到,循环就继续下去。

64

因为那

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

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

哦,请参阅相关文章


也:

如果在end最后一个元素之前,您将如何insert()走到最后呢?


2
这是一个被低估的答案。这些例子简明扼要,直截了当,“ Also”没有被其他人说过,回想起来似乎很明显,但是却像启示一样打动了我。
underscore_d

@underscore_d:谢谢!:)
user541686 '18

顺便说一句,万一我看起来像个虚伪的伪君子,那是因为我早在2016年7月就做过!
underscore_d

@underscore_d:哈哈哈,我什至没有注意到,但是谢谢!:)
user541686 '18

22

半封闭范围的迭代器习惯用法[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)


另外,如果您使用半封闭范围,则可以使用!=运算符检查结束条件,因为(如果运算符定义正确)<暗含!=

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

但是,没有简单的方法可以对全封闭范围执行此操作。你被困住了<=

在C ++中唯一支持<>操作的迭代器是随机访问迭代器。如果您必须为<=C ++中的每个迭代器类编写一个运算符,则必须使所有迭代器具有完全可比性,并且创建功能较弱的迭代器(如on上的双向迭代器std::list或输入迭代器)的选择更少。iostreams如果C ++使用的是全封闭范围,则对进行操作)。


8

通过将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++;
}

0
  1. 如果容器是空的, begin() == end()
  2. C ++程序员倾向于在循环条件下使用!=而不是<(少于),因此,将end()指针指向一个末端是方便的。
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.