为什么标准将end()定义为超过末端的一个,而不是在实际的末端?
为什么标准将end()定义为超过末端的一个,而不是在实际的末端?
因为:
它避免了对空范围的特殊处理。对于空范围,begin()等于 结束()& 它使得遍历元素的循环的结束条件简单:循环简单 只要没有到达end(),就继续。
因为这样
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()的成员返回无效迭代器吗? 两个迭代器已经够痛苦的了……
哦,看看这个相关的帖子。
另外:
如果结束在最后一个元素之前,如何在真正的结束处插入()?!
最好的论点是Dijkstra自己提出的:
你希望范围的大小是一个简单的差异end - begin; 当序列退化为空序列时,包含下界更为“自然”,也因为另一种选择(不包括下界)需要存在“开始前一个”的哨兵值。
你仍然需要证明为什么从0开始计数而不是从1开始,但这不是你问题的一部分。
The wisdom behind the [begin, end) convention pays off time and again when you have any sort of algorithm that deals with multiple nested or iterated calls to range-based constructions, which chain naturally. By contrast, using a doubly-closed range would incur off-by-ones and extremely unpleasant and noisy code. For example, consider a partition [n0, n1)[n1, n2)[n2,n3). Another example is the standard iteration loop for (it = begin; it != end; ++it), which runs end - begin times. The corresponding code would be much less readable if both ends were inclusive – and imagine how you'd handle empty ranges.
最后,我们还可以提出一个很好的论证,为什么计数应该从0开始:根据我们刚刚建立的范围的半开放约定,如果给你一个N个元素的范围(比如枚举数组的成员),那么0是自然的“开始”,这样你就可以把范围写成[0,N),而不需要任何尴尬的偏移或更正。
简而言之:事实上,我们在基于范围的算法中并没有到处看到数字1,这是[begin, end]约定的直接结果和动机。
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++;
}
半封闭范围的迭代器习惯用法[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++中的每个迭代器类编写<=操作符,则必须使所有迭代器完全可比较,并且如果c++使用全封闭范围,则创建功能较差的迭代器(例如std::list上的双向迭代器,或操作iostreams的输入迭代器)的选择就会减少。
实际上,如果你认为迭代器不是指向序列的元素,而是指向序列之间的元素,通过解引用访问下一个元素,那么很多与迭代器相关的东西突然变得更有意义了。然后,“one past end”迭代器突然就有了意义:
+---+---+---+---+
| A | B | C | D |
+---+---+---+---+
^ ^
| |
begin end
显然,begin指向序列的开始,end指向同一序列的结束。取消对begin的引用访问元素A,取消对end的引用没有意义,因为它没有右元素。此外,在中间添加迭代器i会得到
+---+---+---+---+
| A | B | C | D |
+---+---+---+---+
^ ^ ^
| | |
begin i end
你马上就会发现,从开始到i的元素范围包含了元素A和B,而从i到end的元素范围包含了元素C和d。取消引用i给出了它右边的元素,也就是第二个序列的第一个元素。
甚至反向迭代器的“off-by- 1”也突然变得如此明显:
+---+---+---+---+
| D | C | B | A |
+---+---+---+---+
^ ^ ^
| | |
rbegin ri rend
(end) (i) (begin)
我在下面的括号中写了相应的非反向(基)迭代器。你看,属于i的反向迭代器(我命名为ri)仍然指向元素B和c之间。但是由于颠倒了序列,现在元素B在它的右边。