为什么标准将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在它的右边。


如果容器是空的,begin() == end()。 因此,c++程序员倾向于在循环条件中使用!=而不是<(小于) 让end()指向超出末端的位置很方便。