第8.5章: 投影

English

概述

投影是在算法使用某个元素之前,先对作用于该元素的一个函数。算法不直接处理元素,而是处理投影后的值。这样一来,算法就可以基于派生字段或计算出来的值工作,而不必修改底层的数据结构。

投影在概念上很简单:算法看到的是 proj(x) 而不是 x。但要写出正确且高效的代码,理解投影如何与迭代器、引用以及算法语义相互作用是必不可少的。

1. 什么是投影?

投影是一个一元函数,在算法处理每个元素之前先作用于该元素。例如,如果有一个含两个数值字段的结构体,我们可以用投影按其中一个字段排序。


struct Record {
    ::std::size_t index;
    ::std::ptrdiff_t offset;
};

::fast_io::vector<Record> v = {
    {3, -10},
    {1,  25},
    {2,   5}
};

// Sort by the offset field
::std::ranges::sort(v, {}, &Record::offset);
      

算法从不直接比较 Record 对象,它比较的是 &Record::offset 返回的投影值。

2. 为什么需要投影

在没有投影的时候,定制算法通常需要:

投影消除了这些样板代码。它让你能够指定算法该使用元素的哪一部分,而不改变算法如何工作。

3. 投影与比较器的区别

比较器定义两个投影值如何比较;投影定义比较的是元素的哪一部分。


// Comparator only:
::std::ranges::sort(v, [](auto const& a, auto const& b) {
    return a.offset < b.offset;
});

// Comparator + projection:
::std::ranges::sort(v, {}, &Record::offset);
      

投影版本更短,不需要捕获,也不会重复字段名。与 lambda 相比,它还能减小二进制体积。

4. 投影必须是纯函数且开销低

投影会被调用很多次,因此它必须:

投影的行为应当和一次字段访问差不多。任何更昂贵的操作都可能破坏算法性能。


// Bad: expensive projection
::std::ranges::sort(v, {}, [](auto const& r) {
    return r.offset * r.offset * r.offset; // expensive
});
      

这会把 O(n log n) 的排序变成糟糕得多的东西。

5. 投影与引用

投影可以返回引用,但该引用必须在算法整个执行期间保持有效。返回临时量的引用是未定义行为。


// Bad: returns reference to a temporary
::std::ranges::sort(v, ::std::ranges::greater{}, [](auto const& r) -> ::std::ptrdiff_t const& {
    return r.offset + 1; // temporary
});
      

安全的投影返回:

6. 投影与稳定性

投影不会改变算法是否稳定,它只改变算法看到的内容。如果两个元素的投影值相等,稳定算法会保留它们的原始顺序。

要点