初识范围
这一部分开头曾讲过,STL 算法大多作用在序列上。这个序列的正式称呼是范围(Ranges)。C++ 为了让范围有更广泛和灵活的含义,便用迭代器来定义它。
具体来说,任意两个迭代器都可以指代一个范围。
比如这张图画出了两个迭代器 begin 和 end。begin 迭代器指向一系列元素的第一个,end 指向它们的尾后空位。此时,由 begin 和 end 中间“夹”着的元素序列就是它们所指代的范围。
在程序上看,给定两个迭代器 i 和 s,通过
for (; i != s; ++i) {
    *i; // 访问 *i 或 i->foo
}
这种方式访问到的一系列元素,就是 i 和 s 所指代的范围。
此外,如果一个对象可以提供这样的一对迭代器,则称该对象满足范围概念。我有时会简称这样的对象为范围概念。比如 STL 容器 std::vector 等都是范围概念:它们提供的 begin end 成员函数就是一对指代范围的迭代器。数组也是范围概念,对于数组 int a[N]; 来说,&a[0] 和 &a[N] 就是一对迭代器,其所指代的范围是全体数组元素。
我们可以将范围概念传入 STL 算法,比如之前介绍的 rg::sort 和 rg::stable_sort。此外,我们还可以将指代范围的迭代器对传入:
#include <vector>
#include <algorithm>
namespace rg = std::ranges;
int main() {
    std::vector a{4, 1, 6, 2};
    rg::sort(a.begin(), a.end()); // 等价于 rg::sort(a);
}
这种写法应用在数组上非常方便。
#include <iostream>
#include <algorithm>
namespace rg = std::ranges;
int main() {
    int a[10]{4, 1, 6, 2};
    // 只对 a[0] ~ a[3] 排序
    rg::sort(&a[0], &a[4]);
    for (auto i : a) {
        std::cout << i << ' ';
    }
}
在上一节中我只能将 a 整体作为范围概念传入,此时排序的是全体 10 个元素。如果想要对前 4 个元素排序,则必须传入对应的首尾迭代器:首迭代器指向 a[0],尾迭代器指向第四个元素 a[3] 的下一元素,也就是 &a[4]。
 rbegin 与 rend
对于可双向迭代的 STL 容器,除了提供 begin end 这一对迭代器以外,还提供 rbegin 和 rend。简单来说,rbegin rend 所指代的范围也是容器内的全体元素,但遍历的方向是从尾到头。
这张图的粗实线示意了 rbegin 和 rend 的指向。其中 rbegin 指向容器的最后一个元素,rend 指向首元素的前一元素空位。
rbegin 和 rend 可以简化一些用法。比如从大到小排序时,如果传入 rbegin 和 rend,那使用默认的“第二实参” std::less{} 就可以了:
#include <vector>
#include <iostream>
#include <algorithm>
namespace rg = std::ranges;
int main() {
    std::vector a{4, 1, 6, 2};
    rg::sort(a.rbegin(), a.rend());
    for (auto i : a) {
        std::cout << i << ' ';
    }
}
初识视图
在这一节结尾,我略微介绍一下视图(View)。视图是一种特殊的范围,我这里先举几个例子;具体的概念会在之后的章节展开。
这里首先介绍的是 rg::reverse_view。reverse_view 可以将一个范围“反转”,即让 begin 指向末尾元素,end 指向首元素的上一空位,如同 rbegin rend 那样。
#include <vector>
#include <iostream>
#include <algorithm>
#include <ranges> // 视图定义于此
namespace rg = std::ranges;
int main() {
    std::vector a{4, 1, 6, 2};
    rg::sort(rg::reverse_view{a});
    for (auto i : a) {
        std::cout << i << ' ';
    }
}
像这样,rg::reverse_view{a} 就将 a 这个范围概念反转;它的效果和刚才是一致的。
此处使用大括号初始化以避免烦人的分析,尽管目前还不会有。大括号初始化器和小括号初始化器大多情形下可以任意使用(带有
std::initializer_list构造重载的除外,如std::vector)。
有趣的是,标准库定义了一系列重载了 operator| 的对象;它们定义于 std::views 内并且可以生成这样的视图,比如 std::views::reverse 就可以生成 rg::reverse_view。标准上称这些对象为范围适配器对象。不管怎样,这些对象在 operator| 的加成下可以这样用:
#include <vector>
#include <iostream>
#include <algorithm>
#include <ranges> // 视图定义于此
namespace rg = std::ranges;
int main() {
    std::vector a{4, 1, 6, 2};
    rg::sort(a | std::views::reverse);
    for (auto i : a) {
        std::cout << i << ' ';
    }
}
事实上,有了范围适配器之后,很少会在算法中用到裸的迭代器了。刚刚说的数组部分排序问题,也可以通过 rg::take_view 来做。这个适配器只会保留范围中的前 n 个元素,其余元素抛弃。它是这样使用的:
#include <iostream>
#include <algorithm>
#include <ranges>
namespace rg = std::ranges;
int main() {
    int a[10]{4, 1, 6, 2};
    // 只对 a[0] ~ a[3] 排序
    rg::sort(rg::take_view{a, 4});
    // 或者...
    rg::sort(a | std::views::take(4));
    for (auto i : a) {
        std::cout << i << ' ';
    }
}
注意范围适配器对象的版本,operator| 的语法允许你将范围适配器串联起来用:
#include <iostream>
#include <algorithm>
#include <ranges>
namespace rg = std::ranges;
int main() {
    int a[10]{4, 1, 6, 2};
    // 对 a[0] ~ a[3] 倒序排序
    rg::sort(a | std::views::take(4) | std::views::reverse);
    for (auto i : a) {
        std::cout << i << ' ';
    }
}
虽然这些库设施的定义很复杂,但使用起来非常的优雅;甚至性能上也会更好。这也是 STLv2 相比传统版本的改进之处。