为什么第二个容器只传 begin()?

学习 <algorithm> 中的 std::equal 时,遇到了这样的调用:

1
std::equal(v1.begin(), v1.end(), v2.begin());

既然是在比较两个容器的元素,为什么 v1 要传 begin() 和 end(),而 v2 只传一个 begin() 就够了?如果没有 v2.end(),函数怎么知道什么时候结束?

关键在于,这个重载以第一个范围的长度为准。v1.begin() 和 v1.end() 确定要比较的范围,v2.begin() 确定第二个范围的起点;第二个范围需要参与比较的元素数量,由第一个范围决定。标准草案中的定义

两个迭代器同步前进

begin() 指向第一个元素,end() 指向最后一个元素之后的位置,不能解引用。因此 [v1.begin(), v1.end()) 是一个左闭右开的范围。

可以把三个迭代器的版本理解成下面的写法:

1
2
3
4
5
6
7
8
9
10
11
12
template <typename InputIt1, typename InputIt2>
bool equal_demo(InputIt1 first1, InputIt1 last1, InputIt2 first2) {
while (first1 != last1) {
if (!(*first1 == *first2)) {
return false;
}

++first1;
++first2;
}
return true;
}

这里是帮助理解参数含义的示意实现,不是某个标准库的源码,也不能据此认为所有标准库实现都会按这个循环逐项执行。

每比较一对元素,两个迭代器就一起向后移动。第一个迭代器到达 last1,比较就可以结束。若第一个范围有 N 个元素,第二个范围参与比较的就是从 first2 开始的 N 个元素。

所以,不传 v2.end() 并不意味着函数自动知道了 v2 的大小。它只拿到了一个起点,调用者需要保证从这个位置开始有足够的元素。

v2 更长或更短时会怎样?

先看第二个容器更长的情况:

1
2
3
4
5
std::vector<int> v1 = {1, 2, 3};
std::vector<int> v2 = {1, 2, 3, 4};

bool result = std::equal(v1.begin(), v1.end(), v2.begin());
// result 为 true,v2 中最后的 4 不参与比较。

比较关系如下:

1
2
3
v1:  1  2  3
| | |
v2: 1 2 3 4

前三个元素对应相等,函数就返回 true。因此,这个调用不能单独用来判断两个完整容器是否相等。

如果第二个容器更短,例如 v1 是 {1, 2, 3},而 v2 只有 {1, 2},那么调用就不满足这个重载的范围要求,会产生未定义行为。不能把它当成一次安全的比较,也不能指望它自动返回 false。即使某次运行没有崩溃,也不能说明这种用法正确。Microsoft 标准库文档对长度问题的说明

对于这里从 v2.begin() 开始的调用,必须保证:

1
v2.size() >= v1.size()

这个条件保证第二个范围足够长,但要判断两个完整容器相等,还需要它们长度相同。

也可以把两个 end() 都传进去

C++14 起,std::equal 提供了接收四个迭代器的重载:

1
std::equal(v1.begin(), v1.end(), v2.begin(), v2.end());

两个范围都给出了边界,函数就能同时考虑长度和对应元素。长度不同返回 false;长度相同,还要检查对应元素是否相等。四迭代器版本的说明

这也正是当初增加该重载的原因之一:用三个迭代器比较完整序列时,很容易漏掉长度检查,导致错误结果或者未定义行为。相关标准提案 N3671

下面是一个可以直接运行的 C++14 示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <algorithm>
#include <iostream>
#include <vector>

int main() {
std::vector<int> v1 = {1, 2, 3};
std::vector<int> v2 = {1, 2, 3, 4};
std::vector<int> shorter = {1, 2};

std::cout << std::boolalpha;

// 三迭代器版本:比较 v1 全部元素和 v2 的前三个元素。
std::cout << std::equal(v1.begin(), v1.end(), v2.begin()) << '\n';

// 四迭代器版本:长度不同,返回 false。
std::cout << std::equal(v1.begin(), v1.end(),
v2.begin(), v2.end()) << '\n';

// 第二个范围较短也可以安全比较,因为明确提供了结束位置。
std::cout << std::equal(v1.begin(), v1.end(),
shorter.begin(), shorter.end()) << '\n';

// 对于这里同类型的两个 vector,也可以直接比较整个容器。
std::cout << (v1 == v2) << '\n';
}

输出为:

1
2
3
4
true
false
false
false

如果只是在比较这里这样的两个同类型 vector,直接使用 v1 == v2 就很清楚;需要指定比较范围时,再使用 std::equal。

再回头看最初的三个参数,就能对应起来:前两个参数给出第一个完整范围,第三个参数给出第二个范围的起点。第二个范围的比较长度由第一个范围隐含确定,范围是否足够长则由调用者负责。